import { Probot } from 'probot' ; import { Runner } from './runner' ; import { Octokit } from '@octokit/rest' ; import { request } from '@octokit/request' ; import { logger } from 'gcf-utils' ; import { ConfigChecker , getConfig } from '@google-automations/bot-config-utils' ; import { syncLabels } from '@google-automations/label-utils' ; import { Errors , GitHub , Manifest , ManifestOptions , ReleaseType , ReleaserConfig , getReleaserTypes , setLogger , } from 'release-please' ; import schema from './config-schema.json' ; import { BranchConfiguration , ConfigurationOptions , WELL_KNOWN_CONFIGURATION_FILE , DEFAULT_CONFIGURATION , } from './config-constants' ; import { FORCE_RUN_LABEL , RELEASE_PLEASE_LABELS } from './labels' ; type RequestBuilderType = typeof request ; type DefaultFunctionType = RequestBuilderType [ 'defaults' ] ; type RequestFunctionType = ReturnType < DefaultFunctionType > ; type OctokitType = InstanceType < typeof Octokit > ; interface GitHubAPI { graphql : Function ; request : RequestFunctionType ; } function releaseTypeFromRepoLanguage ( language ) { if ( language === null ) { throw Error ( 'repository has no detected language' ) ; } switch ( language . toLowerCase ( ) ) { case 'java' : return 'java-yoshi' ; case 'typescript' : case 'javascript' : return 'node' ; case 'php' : return 'php-yoshi' ; case 'go' : return 'go-yoshi' ; default : { const releasers = getReleaserTypes ( ) ; if ( releasers . includes ( language . toLowerCase ( ) as ReleaseType ) ) { return language . toLowerCase ( ) as ReleaseType ; } else { throw Error ( `unknown release type: ${ language }` ) ; } } } } function findBranchConfiguration ( branch , config ) { const configurations = [ ] ; if ( branch === config . primaryBranch ) { configurations . push ( { ... config , ... { branch } , } ) ; } if ( config . branches ) { for ( const branchConfig of config . branches ) { if ( branch === branchConfig . branch ) { configurations . push ( branchConfig ) ; } } } return configurations ; } param { string } owner param { string } repo param { object } octokit returns { string } async function getRepositoryDefaultBranch ( owner , repo , octokit ) { const { data } = await octokit . repos . get ( { owner , repo , } ) ; return ( data as { default_branch : string ; } ) . default_branch ; } param { string } owner param { string } repo param { object } octokit returns { string } async function getConfigWithDefaultBranch ( owner , repo , octokit , defaultBranch ? ) { const config = await getConfig < ConfigurationOptions > ( octokit , owner , repo , WELL_KNOWN_CONFIGURATION_FILE , { schema : schema } ) ; if ( config && ! config . primaryBranch ) { config . primaryBranch = defaultBranch || ( await getRepositoryDefaultBranch ( owner , repo , octokit ) ) ; } return config ; } async function buildGitHub ( owner , repo , octokit , defaultBranch ? ) { return await GitHub . create ( { owner , repo , defaultBranch , octokitAPIs : { octokit : octokit as { } as OctokitType , request : octokit . request , graphql : octokit . graphql , } , } ) ; } async function buildManifest ( github , repoLanguage , configuration ) { if ( configuration . manifest ) { logger . info ( 'building from manifest file' ) ; return await Manifest . fromManifest ( github , configuration . branch , configuration . manifestConfig , configuration . manifestFile ) ; } const releaseType = configuration . releaseType ? configuration . releaseType : configuration . manifest ? 'simple' : releaseTypeFromRepoLanguage ( repoLanguage ) ; const packageName = configuration . packageName || github . repository . repo ; const releaserConfig = { releaseType , versioning : configuration . versioning , bumpMinorPreMajor : configuration . bumpMinorPreMajor , bumpPatchForMinorPreMajor : configuration . bumpPatchForMinorPreMajor , draft : configuration . draft , draftPullRequest : configuration . draftPullRequest , packageName , includeComponentInTag : ! ! configuration . monorepoTags , pullRequestTitlePattern : configuration . pullRequestTitlePattern , changelogPath : configuration . changelogPath , changelogType : configuration . changelogType , versionFile : configuration . versionFile , extraFiles : configuration . extraFiles , } ; const manifestOverrides = { manifestPath : configuration . manifestFile , labels : configuration . releaseLabels , releaseLabels : configuration . releaseLabel ?. split ( ',' ) , } ; return await Manifest . fromConfig ( github , configuration . branch , releaserConfig , manifestOverrides , configuration . path ) ; } const handler = ( app ) => { app . on ( 'push' , async ( context ) => { const repoUrl = context . payload . repository . full_name ; const branch = context . payload . ref . replace ( 'refs/heads/' , '' ) ; const repoLanguage = context . payload . repository . language ; const { owner , repo } = context . repo ( ) ; const remoteConfiguration = await getConfigWithDefaultBranch ( owner , repo , context . octokit , context . payload . repository . default_branch ) ; if ( ! remoteConfiguration ) { logger . info ( `release-please not configured for (${ repoUrl })` ) ; return ; } const configuration = { ... DEFAULT_CONFIGURATION , ... remoteConfiguration , } ; setLogger ( logger ) ; logger . info ( `push (${ repoUrl }, ${ branch })` ) ; const branchConfigurations = findBranchConfiguration ( branch , configuration ) ; if ( branchConfigurations . length === 0 ) { logger . info ( `no configuration for (${ repoUrl }, ${ branch })` ) ; return ; } const github = await buildGitHub ( owner , repo , context . octokit as GitHubAPI , context . payload . repository . default_branch ) ; for ( const branchConfiguration of branchConfigurations ) { logger . debug ( branchConfiguration ) ; const manifest = await buildManifest ( github , repoLanguage , branchConfiguration ) ; if ( branchConfiguration . handleGHRelease ) { logger . info ( `handling GitHub release for (${ repoUrl })` ) ; try { const numReleases = await Runner . createReleases ( manifest ) ; logger . info ( `Created ${ numReleases } releases` ) ; } catch ( e ) { if ( e instanceof Errors . DuplicateReleaseError ) { logger . warn ( 'Release tag already exists, skipping...' , e ) ; } else { throw e ; } } } try { logger . info ( `creating pull request for (${ repoUrl })` ) ; await Runner . createPullRequests ( manifest ) ; } catch ( e ) { if ( e instanceof Errors . ConfigurationError ) { logger . warn ( e ) ; return ; } else { throw e ; } } } } ) ; app . on ( 'schedule.repository' as any , async ( context ) => { const repoUrl = context . payload . repository . full_name ; const owner = context . payload . organization . login ; const repo = context . payload . repository . name ; const remoteConfiguration = await getConfigWithDefaultBranch ( owner , repo , context . octokit ) ; if ( ! remoteConfiguration ) { logger . info ( `release-please not configured for (${ repoUrl })` ) ; return ; } try { await syncLabels ( context . octokit , owner , repo , RELEASE_PLEASE_LABELS ) ; } catch ( e ) { const err = e as Error ; err . message = `Failed to sync the labels: ${ err . message }` ; logger . error ( err ) ; } } ) ; app . on ( 'pull_request.labeled' , async ( context ) => { if ( ! context . payload . pull_request . labels . some ( ( label ) => label . name === FORCE_RUN_LABEL ) ) { logger . info ( `ignoring non-force label action (${ context . payload . pull_request . labels . map ( label => { return label . name ; } ) . join ( ', ' ) })` ) ; return ; } const repoUrl = context . payload . repository . full_name ; const owner = context . payload . repository . owner . login ; const repo = context . payload . repository . name ; const branch = context . payload . pull_request . base . ref ; const repoLanguage = context . payload . repository . language ; await context . octokit . issues . removeLabel ( { name : FORCE_RUN_LABEL , issue_number : context . payload . pull_request . number , owner , repo , } ) ; const remoteConfiguration = await getConfigWithDefaultBranch ( owner , repo , context . octokit , context . payload . repository . default_branch ) ; if ( ! remoteConfiguration ) { logger . info ( `release-please not configured for (${ repoUrl })` ) ; return ; } const configuration = { ... DEFAULT_CONFIGURATION , ... remoteConfiguration , } ; logger . info ( `pull_request.labeled (${ repoUrl }, ${ branch })` ) ; const branchConfigurations = findBranchConfiguration ( branch , configuration ) ; if ( branchConfigurations . length === 0 ) { logger . info ( `no configuration for (${ repoUrl }, ${ branch })` ) ; return ; } const github = await buildGitHub ( owner , repo , context . octokit as GitHubAPI , context . payload . repository . default_branch ) ; for ( const branchConfiguration of branchConfigurations ) { logger . debug ( branchConfiguration ) ; const manifest = await buildManifest ( github , repoLanguage , branchConfiguration ) ; await Runner . createPullRequests ( manifest ) ; } } ) ; app . on ( 'release.created' , async ( context ) => { const repoUrl = context . payload . repository . full_name ; const { owner , repo } = context . repo ( ) ; const remoteConfiguration = await getConfig < ConfigurationOptions > ( context . octokit , owner , repo , WELL_KNOWN_CONFIGURATION_FILE , { schema : schema } ) ; if ( ! remoteConfiguration ) { logger . info ( `release-please not configured for (${ repoUrl })` ) ; return ; } logger . metric ( 'release_please.release_created' , { url : context . payload . repository . releases_url , } ) ; } ) ; app . on ( [ 'pull_request.opened' , 'pull_request.synchronize' ] , async ( context ) => { const configChecker = new ConfigChecker < ConfigurationOptions > ( schema , WELL_KNOWN_CONFIGURATION_FILE ) ; const { owner , repo } = context . repo ( ) ; await configChecker . validateConfigChanges ( context . octokit , owner , repo , context . payload . pull_request . head . sha , context . payload . pull_request . number ) ; } ) ; app . on ( 'pull_request.closed' , async ( context ) => { const repoUrl = context . payload . repository . full_name ; const { owner , repo } = context . repo ( ) ; const remoteConfiguration = await getConfig < ConfigurationOptions > ( context . octokit , owner , repo , WELL_KNOWN_CONFIGURATION_FILE , { schema : schema } ) ; if ( ! remoteConfiguration ) { logger . info ( `release-please not configured for (${ repoUrl })` ) ; return ; } if ( context . payload . pull_request . merged ) { logger . info ( 'ignoring merged pull request' ) ; return ; } if ( context . payload . pull_request . labels . some ( label => { return label . name === 'autorelease: pending' ; } ) ) { await Promise . all ( [ context . octokit . issues . removeLabel ( context . repo ( { issue_number : context . payload . pull_request . number , name : 'autorelease: pending' , } ) ) , context . octokit . issues . addLabels ( context . repo ( { issue_number : context . payload . pull_request . number , labels : [ 'autorelease: closed' ] , } ) ) , ] ) ; } } ) ; app . on ( 'pull_request.reopened' , async ( context ) => { const repoUrl = context . payload . repository . full_name ; const { owner , repo } = context . repo ( ) ; const remoteConfiguration = await getConfig < ConfigurationOptions > ( context . octokit , owner , repo , WELL_KNOWN_CONFIGURATION_FILE , { schema : schema } ) ; if ( ! remoteConfiguration ) { logger . info ( `release-please not configured for (${ repoUrl })` ) ; return ; } if ( context . payload . pull_request . labels . some ( label => { return label . name === 'autorelease: closed' ; } ) ) { await Promise . all ( [ context . octokit . issues . removeLabel ( context . repo ( { issue_number : context . payload . pull_request . number , name : 'autorelease: closed' , } ) ) , context . octokit . issues . addLabels ( context . repo ( { issue_number : context . payload . pull_request . number , labels : [ 'autorelease: pending' ] , } ) ) , ] ) ; } } ) ; } ; export const api = { handler , getRepositoryDefaultBranch , } ;