Skip to content
This repository was archived by the owner on Sep 13, 2023. It is now read-only.

Commit be92297

Browse files
New state for when a PR is in the process of merging (#110)
* New merging state * Fix tests to include merge callback and new merge state * Minor updates
1 parent d72821b commit be92297

File tree

11 files changed

+158
-146
lines changed

11 files changed

+158
-146
lines changed

src/bitbucket/BitbucketAPI.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ export class BitbucketAPI {
162162
// 5 attempts total
163163
await attemptMerge(1, 4);
164164

165-
// We throw before here if the merge is unsuccessul
165+
// We throw before here if the merge is unsuccessful
166166
Logger.info('Merged Pull Request', {
167167
namespace: 'bitbucket:api:mergePullRequest',
168168
landRequestId,

src/bitbucket/descriptor.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ export const makeDescriptor = () => ({
4545
key: 'atlaskid-addon-panel',
4646
name: {
4747
value: `Landkid Queue${
48-
process.env.LANDKID_DEPLOYMENT !== 'prod' ? ` (${process.env.LANDKID_DEPLOYMENT})` : ''
48+
process.env.LANDKID_DEPLOYMENT !== 'prod'
49+
? ` (${process.env.LANDKID_DEPLOYMENT || 'local'})`
50+
: ''
4951
}`,
5052
},
5153
url:

src/db/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ export class LandRequestStatus extends Model<LandRequestStatus> implements IStat
172172
'queued',
173173
'running',
174174
'awaiting-merge',
175+
'merging',
175176
'success',
176177
'fail',
177178
'aborted',

src/lib/AccountService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export class AccountService {
4343

4444
return JSON.parse(cached);
4545
},
46+
undefined,
4647
);
4748
return info || this.getAccountInfo(aaid, retry - 1);
4849
};

src/lib/Runner.ts

Lines changed: 126 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -215,102 +215,104 @@ export class Runner {
215215
return false; // did not move state, return false
216216
}
217217

218-
// Try to merge PR
219-
try {
220-
// skip CI if there is a dependent PR that is awaiting merge
221-
const skipCI =
222-
dependentsAwaitingMerge.length > 0 &&
223-
this.config.mergeSettings &&
224-
this.config.mergeSettings.skipBuildOnDependentsAwaitingMerge;
225-
const pullRequestId = landRequest.pullRequestId;
226-
Logger.verbose('Attempting merge pull request', {
227-
namespace: 'lib:runner:moveFromAwaitingMerge',
228-
pullRequestId,
229-
landRequestId: landRequest.id,
230-
lockId,
231-
});
232-
await this.client.mergePullRequest(landRequestStatus, { skipCI });
233-
Logger.info('Successfully merged PR', {
234-
namespace: 'lib:runner:moveFromAwaitingMerge',
235-
landRequestId: landRequest.id,
236-
pullRequestId,
237-
lockId,
238-
});
218+
Logger.info('Triggering merge attempt', {
219+
namespace: 'lib:runner:moveFromAwaitingMerge',
220+
pullRequestId: landRequest.pullRequestId,
221+
landRequestId: landRequest.id,
222+
lockId,
223+
});
239224

240-
const end = Date.now();
241-
const queuedDate = await this.getLandRequestQueuedDate(landRequest.id);
242-
const start = queuedDate!.getTime();
243-
eventEmitter.emit('PULL_REQUEST.MERGE.SUCCESS', {
244-
landRequestId: landRequestStatus.requestId,
245-
pullRequestId: landRequest.pullRequestId,
246-
commit: landRequest.forCommit,
247-
sourceBranch: pullRequest.sourceBranch,
248-
targetBranch: pullRequest.targetBranch,
249-
duration: end - start,
250-
});
251-
return landRequest.setStatus('success');
252-
} catch (err) {
253-
eventEmitter.emit('PULL_REQUEST.MERGE.FAIL', {
254-
landRequestId: landRequestStatus.requestId,
255-
pullRequestId: landRequest.pullRequestId,
256-
commit: landRequest.forCommit,
257-
sourceBranch: pullRequest.sourceBranch,
258-
targetBranch: pullRequest.targetBranch,
225+
// Start merge attempt
226+
await landRequest.setStatus('merging');
227+
228+
// skip CI if there is a dependent PR that is awaiting merge
229+
const skipCI =
230+
dependentsAwaitingMerge.length > 0 &&
231+
this.config.mergeSettings &&
232+
this.config.mergeSettings.skipBuildOnDependentsAwaitingMerge;
233+
234+
this.client
235+
.mergePullRequest(landRequestStatus, { skipCI })
236+
.then(async () => {
237+
const end = Date.now();
238+
const queuedDate = await this.getLandRequestQueuedDate(landRequest.id);
239+
const start = queuedDate!.getTime();
240+
eventEmitter.emit('PULL_REQUEST.MERGE.SUCCESS', {
241+
landRequestId: landRequestStatus.requestId,
242+
pullRequestId: landRequest.pullRequestId,
243+
commit: landRequest.forCommit,
244+
sourceBranch: pullRequest.sourceBranch,
245+
targetBranch: pullRequest.targetBranch,
246+
duration: end - start,
247+
});
248+
await landRequest.setStatus('success');
249+
})
250+
.catch(async () => {
251+
eventEmitter.emit('PULL_REQUEST.MERGE.FAIL', {
252+
landRequestId: landRequestStatus.requestId,
253+
pullRequestId: landRequest.pullRequestId,
254+
commit: landRequest.forCommit,
255+
sourceBranch: pullRequest.sourceBranch,
256+
targetBranch: pullRequest.targetBranch,
257+
});
258+
await landRequest.setStatus('fail', 'Unable to merge pull request');
259259
});
260-
return landRequest.setStatus('fail', 'Unable to merge pull request');
261-
}
262260
};
263261

264262
// Next must always return early if ever doing a single state transition
265263
next = async () => {
266-
const runNextAgain = await withLock('status-transition', async (lockId: Date) => {
267-
const queue = await this.queue.getQueue();
268-
Logger.info('Next() called', {
269-
namespace: 'lib:runner:next',
270-
lockId,
271-
queue,
272-
});
264+
const runNextAgain = await withLock(
265+
'status-transition',
266+
async (lockId: Date) => {
267+
const queue = await this.queue.getQueue();
268+
Logger.info('Next() called', {
269+
namespace: 'lib:runner:next',
270+
lockId,
271+
queue,
272+
});
273273

274-
for (const landRequestStatus of queue) {
275-
// Check for this _before_ looking at the state so that we don't have to wait until
276-
const landRequest = landRequestStatus.request;
277-
const failedDeps = await landRequest.getFailedDependencies();
278-
if (failedDeps.length !== 0) {
279-
Logger.info('LandRequest failed due to failing dependency', {
280-
namespace: 'lib:runner:next',
281-
lockId,
282-
landRequestId: landRequest.id,
283-
pullRequestId: landRequest.pullRequestId,
284-
landRequestStatus,
285-
failedDeps,
286-
});
287-
const failedPrIds = failedDeps.map(d => d.request.pullRequestId).join(', ');
288-
const failReason = `Failed due to failed dependency builds: ${failedPrIds}`;
289-
await landRequest.setStatus('fail', failReason);
290-
await landRequest.update({ dependsOn: null });
291-
await this.client.stopLandBuild(landRequest.buildId, lockId);
292-
const user = await this.client.getUser(landRequest.triggererAaid);
293-
return landRequest.setStatus('queued', `Queued by ${user.displayName || user.aaid}`);
294-
}
295-
if (landRequestStatus.state === 'awaiting-merge') {
296-
const awaitingMergeQueue = Runner.getDependentsAwaitingMerge(queue, landRequestStatus);
297-
const didChangeState = await this.moveFromAwaitingMerge(
298-
landRequestStatus,
299-
lockId,
300-
awaitingMergeQueue,
301-
);
302-
// if we moved, we need to exit early, otherwise, just keep checking the queue
303-
if (didChangeState) return true;
304-
} else if (landRequestStatus.state === 'queued') {
305-
const didChangeState = await this.moveFromQueueToRunning(landRequestStatus, lockId);
306-
// if the landrequest was able to move from queued to running, exit early, otherwise, keep
307-
// checking the rest of the queue
308-
if (didChangeState) return true;
274+
for (const landRequestStatus of queue) {
275+
// Check for this _before_ looking at the state so that we don't have to wait until
276+
const landRequest = landRequestStatus.request;
277+
const failedDeps = await landRequest.getFailedDependencies();
278+
if (failedDeps.length !== 0) {
279+
Logger.info('LandRequest failed due to failing dependency', {
280+
namespace: 'lib:runner:next',
281+
lockId,
282+
landRequestId: landRequest.id,
283+
pullRequestId: landRequest.pullRequestId,
284+
landRequestStatus,
285+
failedDeps,
286+
});
287+
const failedPrIds = failedDeps.map(d => d.request.pullRequestId).join(', ');
288+
const failReason = `Failed due to failed dependency builds: ${failedPrIds}`;
289+
await landRequest.setStatus('fail', failReason);
290+
await landRequest.update({ dependsOn: null });
291+
await this.client.stopLandBuild(landRequest.buildId, lockId);
292+
const user = await this.client.getUser(landRequest.triggererAaid);
293+
return landRequest.setStatus('queued', `Queued by ${user.displayName || user.aaid}`);
294+
}
295+
if (landRequestStatus.state === 'awaiting-merge') {
296+
const awaitingMergeQueue = Runner.getDependentsAwaitingMerge(queue, landRequestStatus);
297+
const didChangeState = await this.moveFromAwaitingMerge(
298+
landRequestStatus,
299+
lockId,
300+
awaitingMergeQueue,
301+
);
302+
// if we moved, we need to exit early, otherwise, just keep checking the queue
303+
if (didChangeState) return true;
304+
} else if (landRequestStatus.state === 'queued') {
305+
const didChangeState = await this.moveFromQueueToRunning(landRequestStatus, lockId);
306+
// if the landrequest was able to move from queued to running, exit early, otherwise, keep
307+
// checking the rest of the queue
308+
if (didChangeState) return true;
309+
}
310+
// otherwise, we must just be running, nothing to do here
309311
}
310-
// otherwise, we must just be running, nothing to do here
311-
}
312-
return false;
313-
});
312+
return false;
313+
},
314+
false,
315+
);
314316
if (runNextAgain) {
315317
await this.next();
316318
}
@@ -522,42 +524,46 @@ export class Runner {
522524
};
523525

524526
checkWaitingLandRequests = async () => {
525-
await withLock('status-transition', async () => {
526-
const waitingRequestStatuses = await this.queue.getStatusesForWaitingRequests();
527+
await withLock(
528+
'status-transition',
529+
async () => {
530+
const waitingRequestStatuses = await this.queue.getStatusesForWaitingRequests();
531+
532+
Logger.info('Checking for waiting landrequests ready to queue', {
533+
namespace: 'lib:runner:checkWaitingLandRequests',
534+
waitingRequestStatuses,
535+
});
527536

528-
Logger.info('Checking for waiting landrequests ready to queue', {
529-
namespace: 'lib:runner:checkWaitingLandRequests',
530-
waitingRequestStatuses,
531-
});
537+
for (const landRequestStatus of waitingRequestStatuses) {
538+
const landRequest = landRequestStatus.request;
539+
const pullRequestId = landRequest.pullRequestId;
540+
const triggererUserMode = await permissionService.getPermissionForUser(
541+
landRequest.triggererAaid,
542+
);
543+
const isAllowedToLand = await this.isAllowedToLand(
544+
pullRequestId,
545+
triggererUserMode,
546+
this.getQueue,
547+
);
532548

533-
for (const landRequestStatus of waitingRequestStatuses) {
534-
const landRequest = landRequestStatus.request;
535-
const pullRequestId = landRequest.pullRequestId;
536-
const triggererUserMode = await permissionService.getPermissionForUser(
537-
landRequest.triggererAaid,
538-
);
539-
const isAllowedToLand = await this.isAllowedToLand(
540-
pullRequestId,
541-
triggererUserMode,
542-
this.getQueue,
543-
);
544-
545-
if (isAllowedToLand.errors.length === 0) {
546-
if (isAllowedToLand.existingRequest) {
547-
Logger.warn('Already has existing Land build', {
548-
pullRequestId,
549-
landRequestId: landRequest.id,
550-
landRequestStatus,
551-
namespace: 'lib:runner:checkWaitingLandRequests',
552-
});
553-
await landRequest.setStatus('aborted', 'Already has existing Land build');
554-
continue;
549+
if (isAllowedToLand.errors.length === 0) {
550+
if (isAllowedToLand.existingRequest) {
551+
Logger.warn('Already has existing Land build', {
552+
pullRequestId,
553+
landRequestId: landRequest.id,
554+
landRequestStatus,
555+
namespace: 'lib:runner:checkWaitingLandRequests',
556+
});
557+
await landRequest.setStatus('aborted', 'Already has existing Land build');
558+
continue;
559+
}
560+
const movedState = await this.moveFromWaitingToQueued(landRequestStatus);
561+
if (movedState) return this.next();
555562
}
556-
const movedState = await this.moveFromWaitingToQueued(landRequestStatus);
557-
if (movedState) return this.next();
558563
}
559-
}
560-
});
564+
},
565+
undefined,
566+
);
561567
};
562568

563569
getStatusesForLandRequests = async (

src/lib/utils/locker.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { Logger } from '../Logger';
44

55
const redlock = new RedLock([client]);
66

7-
export const withLock = async <T>(resource: string, fn: (lockId: Date) => Promise<T>) => {
7+
export const withLock = async <T>(
8+
resource: string,
9+
fn: (lockId: Date) => Promise<T>,
10+
fallback: T,
11+
) => {
812
let lock: RedLock.Lock;
913
let lockId: Date;
1014
try {
@@ -15,7 +19,7 @@ export const withLock = async <T>(resource: string, fn: (lockId: Date) => Promis
1519
lockId,
1620
});
1721
} catch {
18-
return;
22+
return fallback;
1923
}
2024
let result: T;
2125
try {

src/static/current-state/components/Header.tsx

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,9 @@ const userInfoStyles = css({
1818
});
1919

2020
const userNameStyles = css({
21-
marginRight: 8,
2221
fontWeight: 'bold',
2322
});
2423

25-
const userImgStyles = css({
26-
height: 32,
27-
width: 32,
28-
borderRadius: '100%',
29-
});
30-
3124
interface HeaderProps {
3225
user?: ISessionUser;
3326
}
@@ -38,10 +31,6 @@ export const Header: React.FunctionComponent<HeaderProps> = ({ user }) => (
3831
{user ? (
3932
<div className={userInfoStyles}>
4033
<span className={userNameStyles}>{user.displayName}</span>
41-
<img
42-
src={`https://bitbucket.org/account/${user.username}/avatar/`}
43-
className={userImgStyles}
44-
/>
4534
</div>
4635
) : null}
4736
</div>

src/static/current-state/components/QueueItem.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,29 +154,32 @@ export const StatusItem: React.FunctionComponent<StatusItemProps> = props => (
154154

155155
const landStatusToAppearance: Record<IStatusUpdate['state'], LozengeAppearance> = {
156156
'will-queue-when-ready': 'new',
157-
'awaiting-merge': 'new',
158157
queued: 'new',
159158
running: 'inprogress',
159+
'awaiting-merge': 'new',
160+
merging: 'new',
160161
success: 'success',
161162
fail: 'removed',
162163
aborted: 'moved',
163164
};
164165

165166
const landStatusToNiceString: Record<IStatusUpdate['state'], string> = {
166167
'will-queue-when-ready': 'Waiting to Land',
167-
'awaiting-merge': 'Awaiting Merge',
168168
queued: 'In Queue',
169169
running: 'Running',
170+
'awaiting-merge': 'Awaiting Merge',
171+
merging: 'Merging',
170172
success: 'Succeeded',
171173
aborted: 'Aborted',
172174
fail: 'Failed',
173175
};
174176

175177
const landStatusToPastTense: Record<IStatusUpdate['state'], string> = {
176178
'will-queue-when-ready': 'Told To Land When Ready',
177-
'awaiting-merge': 'Told to Merge',
178179
queued: 'Told To Land',
179180
running: 'Started',
181+
'awaiting-merge': 'Ready to Merge',
182+
merging: 'Merge Started',
180183
success: 'Succeeded',
181184
fail: 'Failed',
182185
aborted: 'Aborted',

0 commit comments

Comments
 (0)