import { Probot , Context } from 'probot' ; import { createHash } from 'crypto' ; import { Storage } from '@google-cloud/storage' ; import * as util from 'util' ; import { logger } from 'gcf-utils' ; import { request } from 'gaxios' ; const storage = new Storage ( ) ; export interface ConfigurationOptions { ignored ? : boolean ; } const CONFIGURATION_FILE = 'label-sync.yml' ; interface Labels { labels : [ { name : string ; description : string ; color : string ; } ] ; } interface Repo { full_name : string ; } let labelsCache ; async function getLabels ( repoPath ) { if ( ! labelsCache ) { await refreshLabels ( ) ; } const labels = { labels : labelsCache . labels . slice ( 0 ) , } as Labels ; const apiLabelsRes = await getApiLabels ( repoPath ) ; apiLabelsRes . apis . forEach ( api => { if ( ! api . github_label || ! api . api_shortname ) { logger . error ( `
        Missing expected fields for a given API label returned from GCS.
        This object was expected to have a 'github_label' and 'api_shortname'
        property, but it is missing at least one of them.` ) ; logger . error ( util . inspect ( api ) ) ; return ; } labels . labels . push ( { name : api . github_label , description : `Issues related to the ${ api . display_name } API.` , color : createHash ( 'md5' ) . update ( api . api_shortname ) . digest ( 'hex' ) . slice ( 0 , 6 ) , } ) ; } ) ; return labels ; } /**
 * Fetch the list of static labels from this repository, and cache it.
 * Note: this method uses gaxios and a direct HTTP request as opposed to
 * an API call through octokit on purpose :) The GitHub API requires specific
 * permissions to access content in a repository.
 */ async function refreshLabels ( ) { const url = 'https://raw.githubusercontent.com/googleapis/repo-automation-bots/master/packages/label-sync/src/labels.json' ; const res = await request < Labels > ( { url } ) ; labelsCache = res . data ; } export function handler ( app ) { app . on ( [ 'repository.created' , 'repository.transferred' , 'label.edited' , 'label.deleted' , ] , async ( c ) => { const [ owner , repo ] = c . payload . repository . full_name . split ( '/' ) ; let remoteConfiguration = null ; try { remoteConfiguration = await loadConfig ( c ) ; } catch ( e ) { const err = e as Error ; err . message = 'Error reading configuration for ' + `${ c . payload . repository . full_name }, skipping: ${ err . message }` ; logger . error ( err ) ; return ; } if ( remoteConfiguration ?. ignored ) { logger . info ( `skipping repository ${ c . payload . repository . full_name }` ) ; return ; } await reconcileLabels ( c . octokit , owner , repo ) ; } ) ; app . on ( 'schedule.repository' as any , async ( c ) => { const owner = c . payload . organization . login ; const repo = c . payload . repository . name ; let remoteConfiguration = null ; try { remoteConfiguration = await loadConfig ( c ) ; } catch ( e ) { const err = e as Error ; err . message = 'Error reading configuration for ' + `${ c . payload . repository . full_name }, skipping: ${ err . message }` ; logger . error ( err ) ; return ; } if ( remoteConfiguration ?. ignored ) { logger . info ( `skipping repository ${ c . payload . repository . full_name }` ) ; return ; } logger . info ( `running for org ${ c . payload . cron_org }` ) ; if ( c . payload . cron_org !== owner ) { logger . info ( `skipping run for ${ c . payload . cron_org }` ) ; return ; } await reconcileLabels ( c . octokit , owner , repo ) ; } ) ; app . on ( 'installation_repositories.added' , async ( c ) => { await Promise . all ( c . payload . repositories_added . map ( ( r ) => { const [ owner , repo ] = r . full_name . split ( '/' ) ; return reconcileLabels ( c . octokit , owner , repo ) ; } ) ) ; } ) ; } export const loadConfig = async ( context ) => { return ( await context . config ( CONFIGURATION_FILE ) ) as ConfigurationOptions | null ; } ; interface GetApiLabelsResponse { apis : Array < { display_name : string ; github_label : string ; api_shortname : string ; } > ; } interface PublicReposResponse { repos : Array < { repo : string ; github_label : string ; } > ; } param repoPath export const getApiLabels = async ( repoPath ) : Promise < GetApiLabelsResponse > => { const publicRepos = await storage . bucket ( 'devrel-prod-settings' ) . file ( 'public_repos.json' ) . download ( ) ; const { repos } = JSON . parse ( publicRepos [ 0 ] . toString ( ) ) ; const repo = repos . find ( r => r . repo === repoPath && r . github_label !== '' ) ; if ( repo ) { logger . info ( `populating ${ repo . github_label } label for ${ repoPath }` ) ; return { apis : [ { github_label : repo . github_label , api_shortname : repoPath . split ( '/' ) [ 1 ] , display_name : repoPath , } , ] , } ; } logger . info ( `populating all api labels for ${ repoPath }` ) ; const apis = await storage . bucket ( 'devrel-prod-settings' ) . file ( 'apis.json' ) . download ( ) ; const parsedResponse = JSON . parse ( apis [ 0 ] . toString ( ) ) as GetApiLabelsResponse ; logger . info ( `Detected ${ parsedResponse . apis . length } API labels from DRIFT.` ) ; return parsedResponse ; } ; /**
 * Main method. Fetch a list of required labels, and apply them to a given
 * repository.
 */ async function reconcileLabels ( github , owner , repo ) { const newLabels = await getLabels ( `${ owner }/${ repo }` ) ; const oldLabels = await github . paginate ( github . issues . listLabelsForRepo , { owner , repo , per_page : 100 , } ) ; for ( const l of newLabels . labels ) { const match = oldLabels . find ( x => x . name . toLowerCase ( ) === l . name . toLowerCase ( ) ) ; if ( match ) { if ( match . color !== l . color || match . description !== l . description ) { logger . info ( `Updating ${ match . name } from ${ match . color } to ${ l . color } and ${ match . description } to ${ l . description }.` ) ; await github . issues . updateLabel ( { repo , owner , name : l . name , current_name : l . name , description : l . description , color : l . color , } ) . catch ( e => { e . message = `Error updating label ${ l . name } in ${ owner }/${ repo }\n\n${ e . message }` ; logger . error ( e ) ; } ) ; } } else { logger . info ( `Creating label for ${ l . name }.` ) ; await github . issues . createLabel ( { repo , owner , color : l . color , description : l . description , name : l . name , } ) . catch ( e => { if ( ! Array . isArray ( e . errors ) || e . errors [ 0 ] . code !== 'already_exists' ) { e . message = `Error creating label ${ l . name } in ${ owner }/${ repo }\n\n${ e . message }` ; logger . error ( e ) ; } } ) ; } } const labelsToDelete = [ 'bug' , 'enhancement' , 'question' , 'kokoro:force-ci' , 'kokoro: force-run' , 'kokoro: run' , 'buildcop: issue' , 'buildcop: quiet' , 'buildcop: flaky' , ] ; for ( const l of oldLabels ) { if ( labelsToDelete . includes ( l . name ) ) { await github . issues . deleteLabel ( { name : l . name , owner , repo , } ) . then ( ( ) => { logger . info ( `Deleted '${ l . name }' from ${ owner }/${ repo }` ) ; } ) . catch ( e => { e . message = `Error deleting label ${ l . name } in ${ owner }/${ repo }\n\n${ e . message }` ; logger . error ( e ) ; } ) ; } } }