@@ -3,6 +3,8 @@ import path from 'node:path';
33import axios , { AxiosError , AxiosInstance } from 'axios' ;
44import logdown from 'logdown' ;
55
6+ import type { AutoMergeConfig , ActionResult , GitHubPullRequest , Repository , RepositoryResult } from './types/index.js' ;
7+
68interface PackageJson {
79 bin : Record < string , string > ;
810 version : string ;
@@ -14,55 +16,6 @@ const packageJsonPath = path.join(__dirname, '../package.json');
1416const { bin, version : toolVersion } : PackageJson = JSON . parse ( fs . readFileSync ( packageJsonPath , 'utf-8' ) ) ;
1517const toolName = Object . keys ( bin ) [ 0 ] ;
1618
17- /** @see https://docs.github.com/en/rest/reference/pulls#get-a-pull-request */
18- interface GitHubPullRequest {
19- draft : boolean ;
20- head : {
21- /** The branch name */
22- ref : string ;
23- /** The commit SHA-1 hash */
24- sha : string ;
25- } ;
26- /** The pull request number */
27- number : number ;
28- /** The pull request title */
29- title : string ;
30- }
31-
32- export interface ActionResult {
33- error ?: string ;
34- pullNumber : number ;
35- status : 'bad' | 'good' ;
36- }
37-
38- export interface AutoMergeConfig {
39- /** The GitHub auth token */
40- authToken : string ;
41- /** Approve before merging */
42- autoApprove ?: boolean ;
43- /** Don't send any data */
44- dryRun ?: boolean ;
45- /** Merge draft PRs */
46- mergeDrafts ?: boolean ;
47- /** All projects to include */
48- projects : {
49- /** All projects hosted on GitHub in the format `user/repo` */
50- gitHub : string [ ] ;
51- } ;
52- /** Squash when merging */
53- squash ?: boolean ;
54- }
55-
56- export interface Repository {
57- pullRequests : GitHubPullRequest [ ] ;
58- repositorySlug : string ;
59- }
60-
61- export interface RepositoryResult {
62- actionResults : ActionResult [ ] ;
63- repositorySlug : string ;
64- }
65-
6619export class AutoMerge {
6720 private readonly apiClient : AxiosInstance ;
6821 private readonly config : AutoMergeConfig ;
@@ -78,6 +31,7 @@ export class AutoMerge {
7831 this . apiClient = axios . create ( {
7932 baseURL : 'https://api.github.com' ,
8033 headers : {
34+ Accept : 'application/vnd.github+json' ,
8135 Authorization : `token ${ this . config . authToken } ` ,
8236 'User-Agent' : `${ toolName } v${ toolVersion } ` ,
8337 } ,
@@ -110,44 +64,56 @@ export class AutoMerge {
11064 const allRepositories = repositories || ( await this . getRepositoriesWithOpenPullRequests ( ) ) ;
11165 const matchingRepositories = this . getMatchingRepositories ( allRepositories , regex ) ;
11266
113- const resultPromises = matchingRepositories . map ( async ( { pullRequests, repositorySlug} ) => {
114- const actionPromises = pullRequests . map ( pullRequest =>
115- this . approveByPullNumber ( repositorySlug , pullRequest . number )
116- ) ;
117- const actionResults = await Promise . all ( actionPromises ) ;
118- return { actionResults, repositorySlug} ;
119- } ) ;
67+ const processedRepositories : RepositoryResult [ ] = [ ] ;
68+ for ( const { pullRequests, repositorySlug} of matchingRepositories ) {
69+ const actionResults : ActionResult [ ] = [ ] ;
70+ for ( const pullRequest of pullRequests ) {
71+ actionResults . push ( await this . approveByPullNumber ( repositorySlug , pullRequest . number ) ) ;
72+ }
73+ processedRepositories . push ( { actionResults, repositorySlug} ) ;
74+ }
12075
121- return Promise . all ( resultPromises ) ;
76+ return processedRepositories ;
12277 }
12378
12479 private getMatchingRepositories ( repositories : Repository [ ] , regex : RegExp ) : Repository [ ] {
125- return repositories
126- . map ( repository => {
127- const matchingPullRequests = repository . pullRequests . filter ( pullRequest =>
128- new RegExp ( regex ) . test ( pullRequest . head . ref )
129- ) ;
130- if ( matchingPullRequests . length ) {
131- return { pullRequests : matchingPullRequests , repositorySlug : repository . repositorySlug } ;
132- }
133- return undefined ;
134- } )
135- . filter ( Boolean ) as Repository [ ] ;
80+ const matchingRepositories : Repository [ ] = [ ] ;
81+ for ( const repository of repositories ) {
82+ const matchingPullRequests = repository . pullRequests . filter ( pullRequest =>
83+ new RegExp ( regex ) . test ( pullRequest . head . ref )
84+ ) ;
85+ if ( matchingPullRequests . length ) {
86+ matchingRepositories . push ( { pullRequests : matchingPullRequests , repositorySlug : repository . repositorySlug } ) ;
87+ }
88+ }
89+ return matchingRepositories ;
90+ }
91+
92+ private async isPullRequestMergeable ( repositorySlug : string , pullNumber : number ) : Promise < boolean > {
93+ const resourceUrl = `/repos/${ repositorySlug } /pulls/${ pullNumber } ` ;
94+ const response = await this . apiClient . get < GitHubPullRequest > ( resourceUrl ) ;
95+ return response . data . mergeable_state === 'clean' ;
13696 }
13797
13898 async mergeByMatch ( regex : RegExp , repositories ?: Repository [ ] ) : Promise < RepositoryResult [ ] > {
13999 const allRepositories = repositories || ( await this . getRepositoriesWithOpenPullRequests ( ) ) ;
140100 const matchingRepositories = this . getMatchingRepositories ( allRepositories , regex ) ;
141101
142- const resultPromises = matchingRepositories . map ( async ( { pullRequests, repositorySlug} ) => {
143- const actionPromises = pullRequests . map ( pullRequest =>
144- this . mergePullRequest ( repositorySlug , pullRequest . number , this . config . squash )
145- ) ;
146- const actionResults = await Promise . all ( actionPromises ) ;
147- return { actionResults, repositorySlug} ;
148- } ) ;
102+ const processedRepositories : RepositoryResult [ ] = [ ] ;
103+ for ( const { pullRequests, repositorySlug} of matchingRepositories ) {
104+ const actionResults : ActionResult [ ] = [ ] ;
105+ for ( const pullRequest of pullRequests ) {
106+ const isMergeable = this . isPullRequestMergeable ( repositorySlug , pullRequest . number ) ;
107+ if ( ! isMergeable ) {
108+ this . logger . warn ( `Pull request #${ pullRequest . number } in "${ repositorySlug } " is not mergeable. Skipping.` ) ;
109+ continue ;
110+ }
111+ actionResults . push ( await this . mergePullRequest ( repositorySlug , pullRequest . number , this . config . squash ) ) ;
112+ }
113+ processedRepositories . push ( { actionResults, repositorySlug} ) ;
114+ }
149115
150- return Promise . all ( resultPromises ) ;
116+ return processedRepositories ;
151117 }
152118
153119 async approveByPullNumber ( repositorySlug : string , pullNumber : number ) : Promise < ActionResult > {
@@ -189,17 +155,18 @@ export class AutoMerge {
189155 this . checkRepositorySlug ( repositorySlug )
190156 ) ;
191157
192- const repositoriesPromises = repositorySlugs . map ( async repositorySlug => {
158+ const repositories : Repository [ ] = [ ] ;
159+
160+ for ( const repositorySlug of repositorySlugs ) {
193161 try {
194162 const pullRequests = await this . getPullRequestsBySlug ( repositorySlug ) ;
195- return { pullRequests, repositorySlug} ;
163+ repositories . push ( { pullRequests, repositorySlug} ) ;
196164 } catch ( error ) {
197165 this . logger . error ( `Could not get pull requests for "${ repositorySlug } ": ${ ( error as AxiosError ) . message } ` ) ;
198- return undefined ;
199166 }
200- } ) ;
167+ }
201168
202- return ( await Promise . all ( repositoriesPromises ) ) . filter ( Boolean ) as Repository [ ] ;
169+ return repositories ;
203170 }
204171
205172 async getRepositoriesWithOpenPullRequests ( ) : Promise < Repository [ ] > {
@@ -221,11 +188,8 @@ export class AutoMerge {
221188
222189 private async getPullRequestsBySlug ( repositorySlug : string ) : Promise < GitHubPullRequest [ ] > {
223190 const resourceUrl = `/repos/${ repositorySlug } /pulls` ;
224- const params = { state : 'open' } ;
191+ const params = { per_page : 100 , state : 'open' } ;
225192 const response = await this . apiClient . get < GitHubPullRequest [ ] > ( resourceUrl , { params} ) ;
226- if ( this . config . mergeDrafts ) {
227- response . data = response . data . filter ( pr => ! pr . draft ) ;
228- }
229193 return response . data ;
230194 }
231195}
0 commit comments