| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @license | 
					
						
							|  |  |  |  * Copyright Google LLC All Rights Reserved. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Use of this source code is governed by an MIT-style license that can be | 
					
						
							|  |  |  |  * found in the LICENSE file at https://angular.io/license
 | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import * as Octokit from '@octokit/rest'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-27 15:39:31 -07:00
										 |  |  | import {GitClient} from '../../utils/git'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | import {PullRequestFailure} from './failures'; | 
					
						
							|  |  |  | import {matchesPattern} from './string-pattern'; | 
					
						
							| 
									
										
										
										
											2020-07-24 18:05:51 +02:00
										 |  |  | import {getBranchesFromTargetLabel, getTargetLabelFromPullRequest, InvalidTargetBranchError, InvalidTargetLabelError} from './target-label'; | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | import {PullRequestMergeTask} from './task'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** Interface that describes a pull request. */ | 
					
						
							|  |  |  | export interface PullRequest { | 
					
						
							| 
									
										
										
										
											2020-06-16 00:20:36 +02:00
										 |  |  |   /** URL to the pull request. */ | 
					
						
							|  |  |  |   url: string; | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |   /** Number of the pull request. */ | 
					
						
							|  |  |  |   prNumber: number; | 
					
						
							|  |  |  |   /** Title of the pull request. */ | 
					
						
							|  |  |  |   title: string; | 
					
						
							|  |  |  |   /** Labels applied to the pull request. */ | 
					
						
							|  |  |  |   labels: string[]; | 
					
						
							|  |  |  |   /** List of branches this PR should be merged into. */ | 
					
						
							|  |  |  |   targetBranches: string[]; | 
					
						
							|  |  |  |   /** Branch that the PR targets in the Github UI. */ | 
					
						
							|  |  |  |   githubTargetBranch: string; | 
					
						
							|  |  |  |   /** Count of commits in this pull request. */ | 
					
						
							|  |  |  |   commitCount: number; | 
					
						
							|  |  |  |   /** Optional SHA that this pull request needs to be based on. */ | 
					
						
							|  |  |  |   requiredBaseSha?: string; | 
					
						
							|  |  |  |   /** Whether the pull request commit message fixup. */ | 
					
						
							|  |  |  |   needsCommitMessageFixup: boolean; | 
					
						
							| 
									
										
										
										
											2020-06-16 00:20:36 +02:00
										 |  |  |   /** Whether the pull request has a caretaker note. */ | 
					
						
							|  |  |  |   hasCaretakerNote: boolean; | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Loads and validates the specified pull request against the given configuration. | 
					
						
							|  |  |  |  * If the pull requests fails, a pull request failure is returned. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export async function loadAndValidatePullRequest( | 
					
						
							|  |  |  |     {git, config}: PullRequestMergeTask, prNumber: number, | 
					
						
							|  |  |  |     ignoreNonFatalFailures = false): Promise<PullRequest|PullRequestFailure> { | 
					
						
							|  |  |  |   const prData = await fetchPullRequestFromGithub(git, prNumber); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (prData === null) { | 
					
						
							|  |  |  |     return PullRequestFailure.notFound(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const labels = prData.labels.map(l => l.name); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (!labels.some(name => matchesPattern(name, config.mergeReadyLabel))) { | 
					
						
							|  |  |  |     return PullRequestFailure.notMergeReady(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   if (!labels.some(name => matchesPattern(name, config.claSignedLabel))) { | 
					
						
							|  |  |  |     return PullRequestFailure.claUnsigned(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const targetLabel = getTargetLabelFromPullRequest(config, labels); | 
					
						
							|  |  |  |   if (targetLabel === null) { | 
					
						
							|  |  |  |     return PullRequestFailure.noTargetLabel(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const {data: {state}} = | 
					
						
							| 
									
										
										
										
											2020-06-15 09:20:05 -07:00
										 |  |  |       await git.github.repos.getCombinedStatusForRef({...git.remoteParams, ref: prData.head.sha}); | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |   if (state === 'failure' && !ignoreNonFatalFailures) { | 
					
						
							|  |  |  |     return PullRequestFailure.failingCiJobs(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   if (state === 'pending' && !ignoreNonFatalFailures) { | 
					
						
							|  |  |  |     return PullRequestFailure.pendingCiJobs(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const githubTargetBranch = prData.base.ref; | 
					
						
							|  |  |  |   const requiredBaseSha = | 
					
						
							|  |  |  |       config.requiredBaseCommits && config.requiredBaseCommits[githubTargetBranch]; | 
					
						
							|  |  |  |   const needsCommitMessageFixup = !!config.commitMessageFixupLabel && | 
					
						
							|  |  |  |       labels.some(name => matchesPattern(name, config.commitMessageFixupLabel)); | 
					
						
							| 
									
										
										
										
											2020-06-16 00:20:36 +02:00
										 |  |  |   const hasCaretakerNote = !!config.caretakerNoteLabel && | 
					
						
							|  |  |  |       labels.some(name => matchesPattern(name, config.caretakerNoteLabel!)); | 
					
						
							| 
									
										
										
										
											2020-07-24 18:05:51 +02:00
										 |  |  |   let targetBranches: string[]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // If branches are determined for a given target label, capture errors that are
 | 
					
						
							|  |  |  |   // thrown as part of branch computation. This is expected because a merge configuration
 | 
					
						
							|  |  |  |   // can lazily compute branches for a target label and throw. e.g. if an invalid target
 | 
					
						
							|  |  |  |   // label is applied, we want to exit the script gracefully with an error message.
 | 
					
						
							|  |  |  |   try { | 
					
						
							|  |  |  |     targetBranches = await getBranchesFromTargetLabel(targetLabel, githubTargetBranch); | 
					
						
							|  |  |  |   } catch (error) { | 
					
						
							|  |  |  |     if (error instanceof InvalidTargetBranchError || error instanceof InvalidTargetLabelError) { | 
					
						
							|  |  |  |       return new PullRequestFailure(error.failureMessage); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     throw error; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |   return { | 
					
						
							| 
									
										
										
										
											2020-06-16 00:20:36 +02:00
										 |  |  |     url: prData.html_url, | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |     prNumber, | 
					
						
							|  |  |  |     labels, | 
					
						
							|  |  |  |     requiredBaseSha, | 
					
						
							|  |  |  |     githubTargetBranch, | 
					
						
							|  |  |  |     needsCommitMessageFixup, | 
					
						
							| 
									
										
										
										
											2020-06-16 00:20:36 +02:00
										 |  |  |     hasCaretakerNote, | 
					
						
							| 
									
										
										
										
											2020-07-24 18:05:51 +02:00
										 |  |  |     targetBranches, | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |     title: prData.title, | 
					
						
							|  |  |  |     commitCount: prData.commits, | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** Fetches a pull request from Github. Returns null if an error occurred. */ | 
					
						
							|  |  |  | async function fetchPullRequestFromGithub( | 
					
						
							|  |  |  |     git: GitClient, prNumber: number): Promise<Octokit.PullsGetResponse|null> { | 
					
						
							|  |  |  |   try { | 
					
						
							| 
									
										
										
										
											2020-06-15 09:20:05 -07:00
										 |  |  |     const result = await git.github.pulls.get({...git.remoteParams, pull_number: prNumber}); | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |     return result.data; | 
					
						
							|  |  |  |   } catch (e) { | 
					
						
							|  |  |  |     // If the pull request could not be found, we want to return `null` so
 | 
					
						
							|  |  |  |     // that the error can be handled gracefully.
 | 
					
						
							|  |  |  |     if (e.status === 404) { | 
					
						
							|  |  |  |       return null; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     throw e; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** Whether the specified value resolves to a pull request. */ | 
					
						
							|  |  |  | export function isPullRequest(v: PullRequestFailure|PullRequest): v is PullRequest { | 
					
						
							|  |  |  |   return (v as PullRequest).targetBranches !== undefined; | 
					
						
							|  |  |  | } |