import { Storage } from '@google-cloud/storage' ; import { Probot , Context } from 'probot' ; import { logger } from 'gcf-utils' ; import * as helper from './helper' ; import { CONFIG_FILE_NAME , DEFAULT_CONFIGS , LABEL_PRODUCT_BY_DEFAULT , DriftRepo , DriftApi , Label , Config , } from './helper' ; import schema from './config-schema.json' ; import { Endpoints } from '@octokit/types' ; import { ConfigChecker , getConfigWithDefault , } from '@google-automations/bot-config-utils' ; import { syncLabels } from '@google-automations/label-utils' ; type IssueResponse = Endpoints [ 'GET /repos/{owner}/{repo}/issues' ] [ 'response' ] ; const storage = new Storage ( ) ; handler . getDriftFile = async ( file ) => { const bucket = 'devrel-prod-settings' ; const [ contents ] = await storage . bucket ( bucket ) . file ( file ) . download ( ) ; return contents . toString ( ) ; } ; handler . getDriftRepos = async ( ) => { const jsonData = await handler . getDriftFile ( 'public_repos.json' ) ; if ( ! jsonData ) { logger . error ( new Error ( 'public_repos.json downloaded from Cloud Storage was empty' ) ) ; return null ; } return JSON . parse ( jsonData ) . repos as DriftRepo [ ] ; } ; handler . getDriftApis = async ( ) => { const jsonData = await handler . getDriftFile ( 'apis.json' ) ; if ( ! jsonData ) { logger . error ( new Error ( 'apis.json downloaded from Cloud Storage was empty' ) ) ; return null ; } return JSON . parse ( jsonData ) . apis as DriftApi [ ] ; } ; handler . addLabeltoRepoAndIssue = async function addLabeltoRepoAndIssue ( owner , repo , issueNumber , issueTitle , driftRepos , context ) { const driftRepo = driftRepos . find ( x => x . repo === `${ owner }/${ repo }` ) ; const res = await context . octokit . issues . listLabelsOnIssue ( { owner , repo , issue_number : issueNumber , } ) . catch ( logger . error ) ; const labelsOnIssue = res ? res . data : undefined ; let wasNotAdded = true ; let autoDetectedLabel ; if ( ! driftRepo ?. github_label ) { logger . info ( `There was no configured match for the repo ${ repo }, trying to auto-detect the right label` ) ; const apis = await handler . getDriftApis ( ) ; autoDetectedLabel = helper . autoDetectLabel ( apis , issueTitle ) ; } const githubLabel = driftRepo ?. github_label || autoDetectedLabel ; if ( githubLabel ) { if ( labelsOnIssue ) { const foundAPIName = helper . labelExists ( labelsOnIssue , githubLabel ) ; const cleanUpOtherLabels = labelsOnIssue . filter ( ( element ) => element . name ?. startsWith ( 'api' ) && element . name !== foundAPIName ?. name && element . name !== autoDetectedLabel ) ; if ( ! foundAPIName ) { await context . octokit . issues . addLabels ( { owner , repo , issue_number : issueNumber , labels : [ githubLabel ] , } ) . catch ( logger . error ) ; logger . info ( `Label added to ${ owner }/${ repo } for issue ${ issueNumber } is ${ githubLabel }` ) ; wasNotAdded = false ; } for ( const dirtyLabel of cleanUpOtherLabels ) { await context . octokit . issues . removeLabel ( { owner , repo , issue_number : issueNumber , name : dirtyLabel . name , } ) . catch ( logger . error ) ; } } else { await context . octokit . issues . addLabels ( { owner , repo , issue_number : issueNumber , labels : [ githubLabel ] , } ) . catch ( logger . error ) ; logger . info ( `Label added to ${ owner }/${ repo } for issue ${ issueNumber } is ${ githubLabel }` ) ; wasNotAdded = false ; } } if ( ! wasNotAdded ) { if ( driftRepo ?. github_label ) { logger . metric ( 'auto-label.label-added' , { mode : 'repo-config' } ) ; } else if ( autoDetectedLabel ) { logger . metric ( 'auto-label.label-added' , { mode : 'autodetect' } ) ; } } let foundSamplesTag ; if ( labelsOnIssue ) { foundSamplesTag = labelsOnIssue . find ( e => e . name === 'samples' ) ; } const isSampleIssue = repo . includes ( 'samples' ) || issueTitle ?. includes ( 'sample' ) ; if ( ! foundSamplesTag && isSampleIssue ) { await context . octokit . issues . addLabels ( { owner , repo , issue_number : issueNumber , labels : [ 'samples' ] , } ) . catch ( logger . error ) ; logger . info ( `Issue ${ issueNumber } is in a samples repo but does not have a sample tag, adding it to the repo and issue` ) ; logger . metric ( 'auto-label.label-added' , { mode : 'samples' } ) ; wasNotAdded = false ; } return wasNotAdded ; } ; handler . autoLabelOnPR = async function autoLabelOnPR ( context , owner , repo , config ) { if ( config ?. enabled === false ) { logger . info ( `Skipping for ${ owner }/${ repo }` ) ; return ; } const pull_number = context . payload . pull_request . number ; if ( config ?. product ) { const driftRepos = await handler . getDriftRepos ( ) ; if ( ! driftRepos ) { return ; } await handler . addLabeltoRepoAndIssue ( owner , repo , pull_number , context . payload . pull_request . title , driftRepos , context ) ; } if ( ! config ?. path ?. pullrequest && ! config ?. language ?. pullrequest ) { return ; } const filesChanged = await context . octokit . pulls . listFiles ( { owner , repo , pull_number , } ) ; const labels = context . payload . pull_request . labels ; const new_labels = [ ] ; if ( config . path ?. pullrequest ) { logger . info ( `Labeling path in PR #${ pull_number } in ${ owner }/${ repo }...` ) ; const path_label = helper . getLabel ( filesChanged . data , config . path , 'path' ) ; if ( path_label && ! helper . labelExists ( labels , path_label ) ) { logger . info ( `Path label added to PR #${ pull_number } in ${ owner }/${ repo } is ${ path_label }` ) ; new_labels . push ( path_label ) ; } } if ( config . language ?. pullrequest ) { logger . info ( `Labeling language in PR #${ pull_number } in ${ owner }/${ repo }...` ) ; const language_label = helper . getLabel ( filesChanged . data , config . language , 'language' ) ; if ( language_label && ! helper . labelExists ( labels , language_label ) ) { logger . info ( `Language label added to PR #${ pull_number } in ${ owner }/${ repo } is ${ language_label }` ) ; new_labels . push ( language_label ) ; } } if ( new_labels . length ) { logger . info ( `Labels added to PR# ${ pull_number } in ${ owner }/${ repo } are ${ JSON . stringify ( new_labels ) }` ) ; await context . octokit . issues . addLabels ( { owner , repo , issue_number : pull_number , labels : new_labels , } ) ; } } ; /**
 * Function updating a stale label on pull request based on configuration values.
 * Removes previous staleness label if exists
 */ async function updateStalenessLabel ( context , owner , repo , config ) { if ( ! config . staleness ?. pullrequest ) { logger . info ( `Staleness feature is disabled for ${ owner }/${ repo }...` ) ; return ; } const old = config . staleness ?. old || helper . DEFAULT_DAYS_TO_STALE ; const extraold = config . staleness ?. extraold || helper . DEFAULT_DAYS_TO_STALE * 2 ; if ( old > extraold || extraold > helper . MAX_DAYS || old <= 0 ) { logger . info ( `The staleness config is wrong, old = ${ old }, extraold = ${ extraold }, bailing...` ) ; return ; } logger . info ( `Running staleness check for ${ owner }/${ repo }, config: old=${ old }, extraold=${ extraold }...` ) ; for await ( const response of context . octokit . paginate . iterator ( context . octokit . rest . issues . listForRepo , { owner : owner , repo : repo , } ) ) { if ( ! response || ! response . data ) { logger . info ( `No active PRs available in ${ owner }/${ repo }...` ) ; return ; } for ( const pull of response . data ) { if ( ! pull . pull_request ) { continue ; } logger . info ( `Checking staleness in PR #${ pull . number } in ${ owner }/${ repo }...` ) ; let label = null ; const staleLabel = helper . fetchLabelByPrefix ( pull . labels as Label [ ] , helper . STALE_PREFIX ) ; if ( helper . isExpiredByDays ( pull . created_at , extraold ) ) { label = `${ helper . STALE_PREFIX } ${ helper . EXTRAOLD_LABEL }` ; } else if ( helper . isExpiredByDays ( pull . created_at , old ) ) { label = `${ helper . STALE_PREFIX } ${ helper . OLD_LABEL }` ; } if ( label && label !== staleLabel ?. name ) { if ( staleLabel ) { logger . info ( `Deleting ${ staleLabel } in ${ owner }/${ repo }/${ pull . number }...` ) ; context . octokit . issues . removeLabel ( { owner , repo , issue_number : pull . number , name : staleLabel . name , } ) ; } logger . metric ( 'auto-label.label-added' , { repo : `${ owner }/${ repo }/` , number : pull . number , label : label , mode : 'staledetect' , } ) ; logger . info ( `Adding ${ label } to ${ owner }/${ repo }/${ pull . number }...` ) ; await context . octokit . issues . addLabels ( { owner , repo , issue_number : pull . number , labels : [ label ] , } ) ; } } } } /**
 * Function updating a T-shirt size label on pull request if configuration is enabled.
 * Removes previous size label if exists
 */ async function updatePullRequestSizeLabel ( context , owner , repo , config ) { const pull_number = context . payload . pull_request . number ; if ( ! config ?. product || config ?. enabled === false || ! config ?. requestsize ?. enabled ) { logger . info ( `Skipping pull request size calculations for PR#${ pull_number } in ${ owner }/${ repo }` ) ; return ; } const filesChanged = await context . octokit . pulls . listFiles ( { owner , repo , pull_number , } ) ; logger . info ( `Request size calculation in PR #${ pull_number } in ${ owner }/${ repo }...` ) ; let changes = 0 ; filesChanged . data . forEach ( item => { changes += item . changes ; } ) ; logger . info ( `Amount of changes in pull request ${ pull_number } in ${ owner }/${ repo } is ${ changes }` ) ; const pr_size = `${ helper . SIZE_PREFIX } ${ helper . getPullRequestSize ( changes ) }` ; const size_label = helper . fetchLabelByPrefix ( context . payload . pull_request . labels , helper . SIZE_PREFIX ) ; if ( pr_size !== size_label ?. name ) { if ( size_label ) { logger . info ( `Deleting ${ size_label . name } in ${ owner }/${ repo }/${ pull_number }...` ) ; context . octokit . issues . removeLabel ( { owner , repo , issue_number : pull_number , name : size_label . name , } ) ; } logger . info ( `Request size label added to PR #${ pull_number } in ${ owner }/${ repo }, label is "${ pr_size }"` ) ; await context . octokit . issues . addLabels ( { owner , repo , issue_number : pull_number , labels : [ pr_size ] , } ) ; } } /**
 * Main function, responds to label being added
 */ export function handler ( app ) { app . on ( 'schedule.repository' as any , async ( context ) => { const owner = context . payload . organization . login ; const repo = context . payload . repository . name ; const config = await getConfigWithDefault < Config > ( context . octokit , owner , repo , CONFIG_FILE_NAME , DEFAULT_CONFIGS , { schema : schema } ) ; if ( ! config ?. product || config ?. enabled === false ) { logger . info ( `Skipping for ${ owner }/${ repo }` ) ; return ; } if ( config ?. requestsize ?. enabled ) { await syncLabels ( context . octokit , owner , repo , helper . PULL_REQUEST_SIZE_LABELS ) ; } updateStalenessLabel ( context , owner , repo , config ) ; logger . info ( `running for org ${ context . payload . cron_org }` ) ; if ( context . payload . cron_org !== owner ) { logger . info ( `skipping run for ${ context . payload . cron_org }` ) ; return ; } const driftRepos = await handler . getDriftRepos ( ) ; if ( ! driftRepos ) { return ; } const issues = context . octokit . issues . listForRepo . endpoint . merge ( { owner , repo , } ) ; let labelWasNotAddedCount = 0 ; for await ( const response of context . octokit . paginate . iterator ( issues ) ) { const issues = response . data as IssueResponse [ 'data' ] ; for ( const issue of issues ) { const wasNotAdded = await handler . addLabeltoRepoAndIssue ( owner , repo , issue . number , issue . title , driftRepos , context ) ; if ( wasNotAdded ) { logger . info ( `label for ${ issue . number } in ${ owner }/${ repo } was not added` ) ; labelWasNotAddedCount ++ ; } if ( labelWasNotAddedCount > 5 ) { logger . info ( `${ owner / repo } has 5 issues where labels were not added; skipping the rest of this repo check.` ) ; return ; } } } } ) ; app . on ( [ 'issues.opened' , 'issues.reopened' ] , async ( context ) => { const owner = context . payload . repository . owner . login ; const repo = context . payload . repository . name ; const config = await getConfigWithDefault < Config > ( context . octokit , owner , repo , CONFIG_FILE_NAME , DEFAULT_CONFIGS , { schema : schema } ) ; const issueNumber = context . payload . issue . number ; if ( ! config ?. product || config ?. enabled === false ) { logger . info ( `Skipping for ${ owner }/${ repo }` ) ; return ; } const driftRepos = await handler . getDriftRepos ( ) ; if ( ! driftRepos ) { return ; } await handler . addLabeltoRepoAndIssue ( owner , repo , issueNumber , context . payload . issue . title , driftRepos , context ) ; } ) ; app . on ( [ 'pull_request.opened' , 'pull_request.synchronize' ] , async ( context ) => { const owner = context . payload . repository . owner . login ; const repo = context . payload . repository . name ; const configChecker = new ConfigChecker < Config > ( schema , CONFIG_FILE_NAME ) ; await configChecker . validateConfigChanges ( context . octokit , owner , repo , context . payload . pull_request . head . sha , context . payload . pull_request . number ) ; const config = await getConfigWithDefault < Config > ( context . octokit , owner , repo , CONFIG_FILE_NAME , DEFAULT_CONFIGS , { schema : schema } ) ; await updatePullRequestSizeLabel ( context , owner , repo , config ) ; if ( context . payload . action === 'synchronize' ) { return ; } await handler . autoLabelOnPR ( context , owner , repo , config ) ; } ) ; app . on ( [ 'installation.created' ] , async ( context ) => { const repositories = context . payload . repositories ; const driftRepos = await handler . getDriftRepos ( ) ; if ( ! LABEL_PRODUCT_BY_DEFAULT ) return ; if ( ! driftRepos ) return ; if ( ! repositories ) { logger . info ( 'repositories is undefined, exiting' ) ; return ; } for await ( const repository of repositories ) { const [ owner , repo ] = repository . full_name . split ( '/' ) ; const config = await getConfigWithDefault < Config > ( context . octokit , owner , repo , CONFIG_FILE_NAME , DEFAULT_CONFIGS , { schema : schema } ) ; if ( ! config ?. product ) { break ; } for await ( const response of context . octokit . paginate . iterator ( context . octokit . issues . listForRepo , { owner , repo , } ) ) { const issues = response . data ; for ( const issue of issues ) { await handler . addLabeltoRepoAndIssue ( owner , repo , issue . number , issue . title , driftRepos , context ) ; } } } } ) ; }