-
Notifications
You must be signed in to change notification settings - Fork 31
/
merge.ts
159 lines (138 loc) · 4.53 KB
/
merge.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import { getInput } from '@actions/core';
import { getOctokit } from '@actions/github';
import { isMatch } from 'micromatch';
import {
approveAndMergePullRequestMutation,
mergePullRequestMutation,
} from '../graphql/mutations';
import { PullRequestInformationContinuousIntegrationEnd } from '../types';
import { parseInputMergeMethod } from '../utilities/inputParsers';
import { logDebug, logInfo } from '../utilities/log';
import { checkPullRequestTitleForMergePreset } from '../utilities/prTitleParsers';
export interface PullRequestDetails {
commitHeadline: string;
pullRequestId: string;
reviewEdge: { node: { state: string } } | undefined;
}
const EXPONENTIAL_BACKOFF = 2;
const MINIMUM_WAIT_TIME = 1000;
const delay = async (duration: number): Promise<void> =>
new Promise((resolve: () => void): void => {
setTimeout((): void => {
resolve();
}, duration);
});
/**
* Approves and merges a given Pull Request.
*/
const merge = async (
octokit: ReturnType<typeof getOctokit>,
pullRequestDetails: PullRequestDetails,
): Promise<void> => {
const mergeMethod = parseInputMergeMethod();
const { commitHeadline, pullRequestId, reviewEdge } = pullRequestDetails;
const mutation =
reviewEdge === undefined
? approveAndMergePullRequestMutation(mergeMethod)
: mergePullRequestMutation(mergeMethod);
await octokit.graphql(mutation, { commitHeadline, pullRequestId });
};
const shouldRetry = (
error: Error,
retryCount: number,
maximumRetries: number,
): boolean => {
const isRetryableError = error.message.includes('Base branch was modified.');
if (isRetryableError && retryCount > maximumRetries) {
logInfo(
`Unable to merge after ${retryCount.toString()} attempts. Retries exhausted.`,
);
return false;
}
return isRetryableError;
};
const mergeWithRetry = async (
octokit: ReturnType<typeof getOctokit>,
details: PullRequestDetails & {
maximumRetries: number;
retryCount: number;
},
): Promise<void> => {
const { retryCount, maximumRetries } = details;
try {
await merge(octokit, details);
} catch (error: unknown) {
if (shouldRetry(error as Error, retryCount, maximumRetries)) {
const nextRetryIn = retryCount ** EXPONENTIAL_BACKOFF * MINIMUM_WAIT_TIME;
logInfo(`Retrying in ${nextRetryIn.toString()}...`);
await delay(nextRetryIn);
await mergeWithRetry(octokit, {
...details,
maximumRetries,
retryCount: retryCount + 1,
});
return;
}
logInfo(
'An error ocurred while merging the Pull Request. This is usually ' +
'caused by the base branch being out of sync with the target ' +
'branch. In this case, the base branch must be rebased. Some ' +
'tools, such as Dependabot, do that automatically.',
);
/* eslint-disable-next-line @typescript-eslint/no-base-to-string */
logDebug(`Original error: ${(error as Error).toString()}.`);
}
};
export const tryMerge = async (
octokit: ReturnType<typeof getOctokit>,
maximumRetries: number,
{
commitAuthorName,
commitMessageHeadline,
mergeableState,
mergeStateStatus,
merged,
pullRequestId,
pullRequestState,
pullRequestTitle,
reviewEdges,
}: PullRequestInformationContinuousIntegrationEnd,
): Promise<void> => {
const allowedAuthorName = getInput('GITHUB_LOGIN');
const disabledForManualChanges =
getInput('ENABLED_FOR_MANUAL_CHANGES') !== 'true';
if (mergeableState !== 'MERGEABLE') {
logInfo(`Pull request is not in a mergeable state: ${mergeableState}.`);
} else if (merged) {
logInfo(`Pull request is already merged.`);
} else if (
/*
* TODO(@platform) [2021-06-01] Start pulling the value once it reaches
* GA.
*/
mergeStateStatus !== undefined &&
mergeStateStatus !== 'CLEAN'
) {
logInfo(
'Pull request cannot be merged cleanly. ' +
`Current state: ${mergeStateStatus}.`,
);
} else if (pullRequestState !== 'OPEN') {
logInfo(`Pull request is not open: ${pullRequestState}.`);
} else if (checkPullRequestTitleForMergePreset(pullRequestTitle) === false) {
logInfo(`Pull request version bump is not allowed by PRESET.`);
} else if (
isMatch(commitAuthorName, allowedAuthorName) === false &&
disabledForManualChanges === true
) {
logInfo(`Pull request changes were not made by ${allowedAuthorName}.`);
} else {
await mergeWithRetry(octokit, {
commitHeadline: commitMessageHeadline,
maximumRetries,
pullRequestId,
retryCount: 1,
reviewEdge: reviewEdges[0],
});
}
};