Skip to content

Commit 50dfddb

Browse files
authored
High priority queue refactoring & improvements (#67)
1 parent 8ea423f commit 50dfddb

File tree

11 files changed

+206
-130
lines changed

11 files changed

+206
-130
lines changed

.github/workflows/tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333

3434
- name: Check Helm Chart
3535
id: lint
36-
uses: helm/[email protected]alpha.3
36+
uses: helm/[email protected]rc.1
3737
with:
3838
command: lint
3939

@@ -45,6 +45,6 @@ jobs:
4545
if: steps.lint.outputs.changed == 'true'
4646

4747
- name: Check Helm Install
48-
uses: helm/[email protected]alpha.3
48+
uses: helm/[email protected]rc.1
4949
with:
5050
command: install

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Reasons can be for example:
2323
- Autogenerated commit message based on MR title (Suffixed with link to the original MR).
2424
- The blocking jobs are triggered automatically. Very useful for E2E testing etc.
2525
- Bot skips squashing when `bot:skip-squash` label is present on the MR.
26-
- Bot assigns MR to the beginning of the queue when `bot:hi-priority` is present. Useful for hotfixes etc.
26+
- Bot assigns MR to the beginning of the queue when `bot:high-priority` is present. Useful for hotfixes etc.
2727

2828
## Pre-Installation requirements
2929

@@ -92,5 +92,5 @@ GITLAB_AUTH_TOKEN="<token>" yarn run start
9292
| `SQUASH_MERGE_REQUEST` | `true` | It'll squash commits on merge |
9393
| `AUTORUN_MANUAL_BLOCKING_JOBS` | `true` | It'll autorun manual blocking jobs before merge |
9494
| `SKIP_SQUASHING_LABEL` | `bot:skip-squash` | It'll skip squash when MR contains this label |
95-
| `HI_PRIORITY_LABEL` | `bot:hi-priority` | It'll put MR with this label to the beginning of the queue |
95+
| `HI_PRIORITY_LABEL` | `bot:high-priority` | It'll put MR with this label to the beginning of the queue |
9696
| `SENTRY_DSN` | `` | It'll enable Sentry monitoring |

charts/gitlab-merger-bot/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ apiVersion: v1
22
appVersion: "1.0"
33
description: A Helm chart for Kubernetes
44
name: gitlab-merger-bot
5-
version: 1.2.3
5+
version: 1.2.4
66
home: https://github.com/pepakriz/gitlab-merger-bot
77
maintainers:
88
- name: pepakriz

charts/gitlab-merger-bot/templates/deployment.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ spec:
5555
value: "{{ .Values.settings.sentryDsn }}"
5656
- name: SKIP_SQUASHING_LABEL
5757
value: "{{ .Values.settings.skipSquashingLabel }}"
58-
- name: HI_PRIORITY_LABEL
59-
value: "{{ .Values.settings.hiPriorityLabel }}"
58+
- name: HIGH_PRIORITY_LABEL
59+
value: "{{ .Values.settings.highPriorityLabel }}"
6060
resources:
6161
{{ toYaml .Values.resources | indent 10 }}
6262
{{- if .Values.nodeSelector }}

charts/gitlab-merger-bot/values.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@ settings:
3030
squashMergeRequest: true
3131
autorunManualBlockingJobs: true
3232
skipSquashingLabel: ""
33-
hiPriorityLabel: ""
33+
highPriorityLabel: ""

src/MergeRequestAcceptor.ts

Lines changed: 108 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export enum AcceptMergeRequestResultKind {
2626
WaitingForApprovals,
2727
UnresolvedDiscussion,
2828
Unauthorized,
29+
InvalidSha,
2930
}
3031

3132
interface Response {
@@ -92,6 +93,11 @@ interface UnauthorizedResponse extends Response {
9293
mergeRequestInfo: MergeRequestInfo;
9394
}
9495

96+
interface InvalidShaResponse extends Response {
97+
kind: AcceptMergeRequestResultKind.InvalidSha;
98+
mergeRequestInfo: MergeRequestInfo;
99+
}
100+
95101
export type AcceptMergeRequestResult = SuccessResponse
96102
| ClosedMergeRequestResponse
97103
| ReassignedMergeRequestResponse
@@ -104,6 +110,11 @@ export type AcceptMergeRequestResult = SuccessResponse
104110
| UnresolvedDiscussionResponse
105111
| UnauthorizedResponse;
106112

113+
export type MergeMergeRequestResult = SuccessResponse
114+
| CanNotBeMergedResponse
115+
| InvalidShaResponse
116+
| UnauthorizedResponse;
117+
107118
interface AcceptMergeRequestOptions {
108119
ciInterval: number;
109120
removeBranchAfterMerge: boolean;
@@ -140,23 +151,97 @@ export const filterBotLabels = (labels: string[]): string[] => {
140151
return labels.filter((label) => !values.includes(label));
141152
};
142153

143-
export const acceptMergeRequest = async (gitlabApi: GitlabApi, mergeRequest: MergeRequest, user: User, options: AcceptMergeRequestOptions): Promise<AcceptMergeRequestResult> => {
144-
let mergeRequestInfo: MergeRequestInfo;
154+
export const acceptMergeRequest = async (
155+
gitlabApi: GitlabApi,
156+
mergeRequest: MergeRequest,
157+
user: User,
158+
options: AcceptMergeRequestOptions,
159+
): Promise<MergeMergeRequestResult> => {
160+
console.log(`[MR][${mergeRequest.iid}] Calling merge request`);
161+
const mergeRequestInfo = await gitlabApi.getMergeRequestInfo(mergeRequest.project_id, mergeRequest.iid);
162+
const response = await gitlabApi.sendRawRequest(`/api/v4/projects/${mergeRequestInfo.project_id}/merge_requests/${mergeRequestInfo.iid}/merge`, RequestMethod.Put, {
163+
should_remove_source_branch: options.removeBranchAfterMerge,
164+
sha: mergeRequestInfo.diff_refs.head_sha,
165+
squash: mergeRequestInfo.labels.includes(options.skipSquashingLabel) ? false : options.squashMergeRequest,
166+
squash_commit_message: `${mergeRequestInfo.title} (!${mergeRequestInfo.iid})`,
167+
merge_commit_message: `${mergeRequestInfo.title} (!${mergeRequestInfo.iid})`,
168+
});
169+
170+
if (response.status === 405) {
171+
console.log(`[MR][${mergeRequestInfo.iid}] 405 - cannot be merged`);
172+
return {
173+
kind: AcceptMergeRequestResultKind.CanNotBeMerged,
174+
mergeRequestInfo,
175+
user,
176+
};
177+
}
178+
179+
if (response.status === 406) {
180+
console.log(`[MR][${mergeRequestInfo.iid}] 406 - already merged`);
181+
return {
182+
kind: AcceptMergeRequestResultKind.SuccessfullyMerged,
183+
mergeRequestInfo,
184+
user,
185+
};
186+
}
187+
188+
if (response.status === 409) {
189+
console.log(`[MR][${mergeRequestInfo.iid}] 409 - SHA does not match HEAD of source branch`);
190+
return {
191+
kind: AcceptMergeRequestResultKind.InvalidSha,
192+
mergeRequestInfo,
193+
user,
194+
};
195+
}
196+
197+
if (response.status === 401) {
198+
console.log(`[MR][${mergeRequestInfo.iid}] 409 - Unauthorized`);
199+
return {
200+
kind: AcceptMergeRequestResultKind.Unauthorized,
201+
mergeRequestInfo,
202+
user,
203+
};
204+
}
205+
206+
if (response.status !== 200) {
207+
throw new Error(`Unsupported response status ${response.status}`);
208+
}
209+
210+
const data = await response.json();
211+
if (typeof data !== 'object' && data.id === undefined) {
212+
console.error('response', data);
213+
throw new Error('Invalid response');
214+
}
215+
216+
if (!containsLabel(mergeRequestInfo.labels, BotLabels.Accepting)) {
217+
await gitlabApi.updateMergeRequest(mergeRequestInfo.project_id, mergeRequestInfo.iid, {
218+
labels: [...filterBotLabels(mergeRequestInfo.labels), BotLabels.Accepting].join(','),
219+
});
220+
}
221+
222+
return {
223+
kind: AcceptMergeRequestResultKind.SuccessfullyMerged,
224+
mergeRequestInfo,
225+
user,
226+
};
227+
};
228+
229+
export const runAcceptingMergeRequest = async (gitlabApi: GitlabApi, mergeRequest: MergeRequest, user: User, options: AcceptMergeRequestOptions): Promise<AcceptMergeRequestResult> => {
145230
let numberOfPipelineValidationRetries = defaultPipelineValidationRetries;
146231
let numberOfRebasingRetries = defaultRebasingRetries;
147232

148233
while (true) {
149-
const tasks: Promise<any>[] = [sleep(options.ciInterval)];
150-
mergeRequestInfo = await gitlabApi.getMergeRequestInfo(mergeRequest.project_id, mergeRequest.iid);
151-
152-
if (!containsAssignedUser(mergeRequestInfo, user)) {
153-
return {
154-
kind: AcceptMergeRequestResultKind.ReassignedMergeRequest,
155-
mergeRequestInfo,
156-
user,
157-
};
234+
const mergeResponse = await acceptMergeRequest(gitlabApi, mergeRequest, user, options);
235+
if (
236+
mergeResponse.kind === AcceptMergeRequestResultKind.SuccessfullyMerged
237+
|| mergeResponse.kind === AcceptMergeRequestResultKind.Unauthorized
238+
) {
239+
return mergeResponse;
158240
}
159241

242+
const mergeRequestInfo = mergeResponse.mergeRequestInfo;
243+
const tasks: Promise<any>[] = [sleep(options.ciInterval)];
244+
160245
if (mergeRequestInfo.state === MergeState.Merged) {
161246
return {
162247
kind: AcceptMergeRequestResultKind.SuccessfullyMerged,
@@ -185,13 +270,11 @@ export const acceptMergeRequest = async (gitlabApi: GitlabApi, mergeRequest: Mer
185270
};
186271
}
187272

188-
const approvals = await gitlabApi.getMergeRequestApprovals(mergeRequestInfo.project_id, mergeRequestInfo.iid);
189-
if (approvals.approvals_left > 0) {
273+
if (!containsAssignedUser(mergeRequestInfo, user)) {
190274
return {
191-
kind: AcceptMergeRequestResultKind.WaitingForApprovals,
275+
kind: AcceptMergeRequestResultKind.ReassignedMergeRequest,
192276
mergeRequestInfo,
193277
user,
194-
approvals,
195278
};
196279
}
197280

@@ -224,6 +307,16 @@ export const acceptMergeRequest = async (gitlabApi: GitlabApi, mergeRequest: Mer
224307
};
225308
}
226309

310+
const approvals = await gitlabApi.getMergeRequestApprovals(mergeRequestInfo.project_id, mergeRequestInfo.iid);
311+
if (approvals.approvals_left > 0) {
312+
return {
313+
kind: AcceptMergeRequestResultKind.WaitingForApprovals,
314+
mergeRequestInfo,
315+
user,
316+
approvals,
317+
};
318+
}
319+
227320
if (mergeRequestInfo.diverged_commits_count > 0) {
228321
if (numberOfRebasingRetries <= 0 && mergeRequestInfo.merge_error !== null) {
229322
console.log(`[MR][${mergeRequestInfo.iid}] Merge error after rebase`);
@@ -354,57 +447,6 @@ export const acceptMergeRequest = async (gitlabApi: GitlabApi, mergeRequest: Mer
354447
}
355448
}
356449

357-
console.log(`[MR][${mergeRequestInfo.iid}] Calling merge request`);
358-
const response = await gitlabApi.sendRawRequest(`/api/v4/projects/${mergeRequestInfo.project_id}/merge_requests/${mergeRequestInfo.iid}/merge`, RequestMethod.Put, {
359-
should_remove_source_branch: options.removeBranchAfterMerge,
360-
sha: mergeRequestInfo.diff_refs.head_sha,
361-
squash: mergeRequestInfo.labels.includes(options.skipSquashingLabel) ? false : options.squashMergeRequest,
362-
squash_commit_message: `${mergeRequestInfo.title} (!${mergeRequestInfo.iid})`,
363-
merge_commit_message: `${mergeRequestInfo.title} (!${mergeRequestInfo.iid})`,
364-
});
365-
366-
if (response.status === 405) {
367-
console.log(`[MR][${mergeRequestInfo.iid}] 405 - cannot be merged`);
368-
continue;
369-
}
370-
371-
if (response.status === 406) {
372-
console.log(`[MR][${mergeRequestInfo.iid}] 406 - already merged`);
373-
continue;
374-
}
375-
376-
if (response.status === 409) {
377-
console.log(`[MR][${mergeRequestInfo.iid}] 409 - SHA does not match HEAD of source branch`);
378-
continue;
379-
}
380-
381-
if (response.status === 401) {
382-
return {
383-
kind: AcceptMergeRequestResultKind.Unauthorized,
384-
mergeRequestInfo,
385-
user,
386-
};
387-
}
388-
389-
if (response.status !== 200) {
390-
throw new Error(`Unsupported response status ${response.status}`);
391-
}
392-
393-
const data = await response.json();
394-
if (typeof data !== 'object' && data.id === undefined) {
395-
console.error('response', data);
396-
throw new Error('Invalid response');
397-
}
398-
399-
if (!containsLabel(mergeRequestInfo.labels, BotLabels.Accepting)) {
400-
tasks.push(
401-
gitlabApi.updateMergeRequest(mergeRequestInfo.project_id, mergeRequestInfo.iid, {
402-
labels: [...filterBotLabels(mergeRequestInfo.labels), BotLabels.Accepting].join(','),
403-
}),
404-
);
405-
}
406-
407-
console.log(`[MR][${mergeRequestInfo.iid}] Merge request is processing`);
408450
await Promise.all(tasks);
409451
}
410452
};

src/Queue.ts

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export enum JobPriority {
2-
HI,
3-
NORMAL,
2+
HIGH = 'high',
3+
NORMAL = 'normal',
44
}
55

66
interface Jobs {
@@ -11,34 +11,45 @@ export class Queue {
1111

1212
private promise?: Promise<void>;
1313
private jobs: { [key in JobPriority]: Jobs } = {
14-
[JobPriority.HI]: {},
14+
[JobPriority.HIGH]: {},
1515
[JobPriority.NORMAL]: {},
1616
};
1717

18-
public hasJob(
18+
public setJobPriority(
1919
jobId: string,
2020
jobPriority: JobPriority,
2121
): boolean {
22-
const currentJobPriority = this.getPriorityByJobId(jobId);
22+
const currentJobPriority = this.findPriorityByJobId(jobId);
2323
if (currentJobPriority === null) {
2424
return false;
2525
}
2626

27-
if (currentJobPriority === JobPriority.NORMAL && jobPriority === JobPriority.HI) {
28-
console.log(`[job][${jobId}] Job has been moved to the prioritized queue`);
27+
if (currentJobPriority === JobPriority.NORMAL && jobPriority === JobPriority.HIGH) {
2928
this.jobs[jobPriority][jobId] = this.jobs[currentJobPriority][jobId];
3029
delete this.jobs[currentJobPriority][jobId];
3130
}
3231

3332
return true;
3433
}
3534

35+
public findPriorityByJobId(jobId: string): JobPriority | null {
36+
if (typeof this.jobs[JobPriority.HIGH][jobId] !== 'undefined') {
37+
return JobPriority.HIGH;
38+
}
39+
40+
if (typeof this.jobs[JobPriority.NORMAL][jobId] !== 'undefined') {
41+
return JobPriority.NORMAL;
42+
}
43+
44+
return null;
45+
}
46+
3647
public runJob<T extends Promise<any>>(
3748
jobId: string,
3849
job: () => T,
3950
jobPriority: JobPriority,
4051
): T {
41-
const currentJobPriority = this.getPriorityByJobId(jobId);
52+
const currentJobPriority = this.findPriorityByJobId(jobId);
4253
if (currentJobPriority !== null) {
4354
throw new Error(`JobId ${jobId} is already in queue`);
4455
}
@@ -51,7 +62,7 @@ export class Queue {
5162
reject(e);
5263
}
5364

54-
const runtimeJobPriority = this.getPriorityByJobId(jobId);
65+
const runtimeJobPriority = this.findPriorityByJobId(jobId);
5566
if (runtimeJobPriority === null) {
5667
throw new Error(`JobId ${jobId} not found`);
5768
}
@@ -65,8 +76,8 @@ export class Queue {
6576
if (this.promise === undefined) {
6677
this.promise = new Promise(async (resolve, reject) => {
6778
while (true) {
68-
let jobIds = Object.keys(this.jobs[JobPriority.HI]);
69-
let priority = JobPriority.HI;
79+
let jobIds = Object.keys(this.jobs[JobPriority.HIGH]);
80+
let priority = JobPriority.HIGH;
7081

7182
if (jobIds.length === 0) {
7283
jobIds = Object.keys(this.jobs[JobPriority.NORMAL]);
@@ -93,16 +104,4 @@ export class Queue {
93104
return jobPromise as T;
94105
}
95106

96-
private getPriorityByJobId(jobId: string): JobPriority | null {
97-
if (typeof this.jobs[JobPriority.HI][jobId] !== 'undefined') {
98-
return JobPriority.HI;
99-
}
100-
101-
if (typeof this.jobs[JobPriority.NORMAL][jobId] !== 'undefined') {
102-
return JobPriority.NORMAL;
103-
}
104-
105-
return null;
106-
}
107-
108107
}

0 commit comments

Comments
 (0)