Skip to content

Commit 66bb2a1

Browse files
committed
Cancel build/release on delete
1 parent 9fc7e23 commit 66bb2a1

9 files changed

Lines changed: 164 additions & 18 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,8 @@ Create the following policies:
354354
"codebuild:CreateProject",
355355
"codebuild:BatchGetProjects",
356356
"codebuild:BatchGetBuilds",
357-
"codebuild:StartBuild"
357+
"codebuild:StartBuild",
358+
"codebuild:StopBuild"
358359
],
359360
"Resource": ""
360361
},

src/lib/server/aws/codebuild.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
CreateProjectCommand,
77
type ProjectCache,
88
type ProjectSource,
9-
StartBuildCommand
9+
StartBuildCommand,
10+
StopBuildCommand
1011
} from '@aws-sdk/client-codebuild';
1112
import { SpanStatusCode, trace } from '@opentelemetry/api';
1213
import type { Prisma } from '@prisma/client';
@@ -228,6 +229,28 @@ export class CodeBuild extends AWSCommon {
228229
});
229230
}
230231

232+
public async cancelBuild(guid: string, buildProcess: string) {
233+
return tracer.startActiveSpan(`CodeBuild - CancelBuild`, async (span) => {
234+
try {
235+
const existing = await this.getBuildStatus(guid, buildProcess);
236+
if (existing?.id) {
237+
span.setAttribute('code-build.build-status', this.getStatus(existing) ?? '(missing)');
238+
await this.codeBuildClient.send(new StopBuildCommand({ id: existing.id }));
239+
}
240+
241+
return existing;
242+
} catch (e) {
243+
span.recordException(e as Error);
244+
span.setStatus({
245+
code: SpanStatusCode.ERROR,
246+
message: (e as Error).message
247+
});
248+
} finally {
249+
span.end();
250+
}
251+
});
252+
}
253+
231254
/**
232255
* This method returns the build status object
233256
*

src/lib/server/bullmq/BullWorker.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ export class Builds<J extends BullMQ.BuildJob> extends BullWorker<J> {
116116
switch (job.data.type) {
117117
case BullMQ.JobType.Build_Product:
118118
return Executor.Build.product(job as Job<BullMQ.Build.Product>);
119+
case BullMQ.JobType.Build_Cancel:
120+
return Executor.Build.cancel(job as Job<BullMQ.Build.Cancel>);
119121
}
120122
}
121123
}
@@ -154,6 +156,8 @@ export class Releases<J extends BullMQ.PublishJob> extends BullWorker<J> {
154156
switch (job.data.type) {
155157
case BullMQ.JobType.Release_Product:
156158
return Executor.Release.product(job as Job<BullMQ.Release.Product>);
159+
case BullMQ.JobType.Release_Cancel:
160+
return Executor.Release.cancel(job as Job<BullMQ.Release.Cancel>);
157161
}
158162
}
159163
}

src/lib/server/bullmq/types.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-namespace */
22
import type { RepeatOptions } from 'bullmq';
3+
import type { BuildForPrefix, ReleaseForPrefix } from '../models/artifacts';
34

45
/** Retry a job for 72 hours every 10 minutes. Useful for build engine tasks */
56
export const Retry0f600 = {
@@ -28,13 +29,15 @@ export enum QueueName {
2829
export enum JobType {
2930
// Build Jobs
3031
Build_Product = 'Build Product',
32+
Build_Cancel = 'Cancel Build',
3133
// Polling Jobs
3234
Poll_Build = 'Check Product Build',
3335
Poll_Release = 'Check Product Release',
3436
// Project Jobs
3537
Project_Create = 'Create Project',
3638
// Publishing Jobs
3739
Release_Product = 'Release Product',
40+
Release_Cancel = 'Cancel Release',
3841
// S3 Jobs
3942
S3_CopyArtifacts = 'Copy Artifacts to S3',
4043
S3_CopyError = 'Copy Errors to S3',
@@ -52,6 +55,12 @@ export namespace Build {
5255
type: JobType.Build_Product;
5356
buildId: number;
5457
}
58+
59+
export interface Cancel {
60+
type: JobType.Build_Cancel;
61+
guid: string;
62+
build: BuildForPrefix;
63+
}
5564
}
5665

5766
export namespace Polling {
@@ -78,6 +87,12 @@ export namespace Release {
7887
type: JobType.Release_Product;
7988
releaseId: number;
8089
}
90+
91+
export interface Cancel {
92+
type: JobType.Release_Cancel;
93+
guid: string;
94+
release: ReleaseForPrefix;
95+
}
8196
}
8297

8398
export namespace S3 {
@@ -104,9 +119,9 @@ export namespace System {
104119

105120
export type Job = JobTypeMap[keyof JobTypeMap];
106121

107-
export type BuildJob = JobTypeMap[JobType.Build_Product];
122+
export type BuildJob = JobTypeMap[JobType.Build_Product | JobType.Build_Cancel];
108123
export type S3Job = JobTypeMap[JobType.S3_CopyArtifacts | JobType.S3_CopyError];
109-
export type PublishJob = JobTypeMap[JobType.Release_Product];
124+
export type PublishJob = JobTypeMap[JobType.Release_Product | JobType.Release_Cancel];
110125
export type PollJob = JobTypeMap[JobType.Poll_Build | JobType.Poll_Release];
111126
export type ProjectJob = JobTypeMap[JobType.Project_Create];
112127
export type StartupJob = JobTypeMap[
@@ -116,10 +131,12 @@ export type RecurringJob = JobTypeMap[JobType.System_RefreshAppVersions];
116131

117132
export type JobTypeMap = {
118133
[JobType.Build_Product]: Build.Product;
134+
[JobType.Build_Cancel]: Build.Cancel;
119135
[JobType.Poll_Build]: Polling.Build;
120136
[JobType.Poll_Release]: Polling.Release;
121137
[JobType.Project_Create]: Project.Create;
122138
[JobType.Release_Product]: Release.Product;
139+
[JobType.Release_Cancel]: Release.Cancel;
123140
[JobType.S3_CopyArtifacts]: S3.CopyArtifacts;
124141
[JobType.S3_CopyError]: S3.CopyErrors;
125142
[JobType.System_CreateCodeBuildProject]: System.CreateCodeBuildProject;

src/lib/server/job-executors/build.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { readFile } from 'node:fs/promises';
44
import { join } from 'node:path';
55
import { CodeBuild } from '../aws/codebuild';
66
import { CodeCommit } from '../aws/codecommit';
7+
import { S3 } from '../aws/s3';
78
import { BullMQ, getQueues } from '../bullmq';
89
import { Build } from '../models/build';
910
import { prisma } from '../prisma';
@@ -78,7 +79,7 @@ export async function product(job: Job<BullMQ.Build.Product>): Promise<unknown>
7879
)
7980
});
8081
}
81-
const name = `Check status of Build #${build.id}`;
82+
const name = pollName(build.id);
8283
await getQueues().Polling.upsertJobScheduler(name, BullMQ.RepeatEveryMinute, {
8384
name,
8485
data: {
@@ -127,7 +128,7 @@ export async function product(job: Job<BullMQ.Build.Product>): Promise<unknown>
127128
)
128129
});
129130
}
130-
const name = `Check status of Build #${build.id}`;
131+
const name = pollName(build.id);
131132
await getQueues().Polling.upsertJobScheduler(name, BullMQ.RepeatEveryMinute, {
132133
name,
133134
data: {
@@ -158,6 +159,23 @@ export async function product(job: Job<BullMQ.Build.Product>): Promise<unknown>
158159
}
159160
}
160161

162+
export async function cancel(job: Job<BullMQ.Build.Cancel>): Promise<unknown> {
163+
const pollRemoved = await getQueues().Polling.removeJobScheduler(pollName(job.data.build.id));
164+
job.updateProgress(10);
165+
const codeBuild = new CodeBuild();
166+
job.updateProgress(20);
167+
const build = await codeBuild.cancelBuild(
168+
job.data.guid,
169+
CodeBuild.getCodeBuildProjectName('build_app')
170+
);
171+
job.updateProgress(50);
172+
const s3 = new S3();
173+
job.updateProgress(60);
174+
const objects = await s3.removeCodeBuildFolder(job.data.build);
175+
job.updateProgress(100);
176+
return { pollRemoved, build, objects };
177+
}
178+
161179
async function getVersionCode(
162180
job: Prisma.jobGetPayload<{ select: { id: true; existing_version_code: true } }>
163181
) {
@@ -173,3 +191,7 @@ async function getVersionCode(
173191
});
174192
return build._max.version_code ?? job.existing_version_code ?? 0;
175193
}
194+
195+
function pollName(id: number) {
196+
return `Check status of Build #${id}`;
197+
}

src/lib/server/job-executors/polling.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export async function build(job: Job<BullMQ.Polling.Build>): Promise<unknown> {
7171
}
7272
} catch (e) {
7373
job.log(`${e}`);
74-
await prisma.build.update({
74+
await prisma.build.updateMany({
7575
where: { id: job.data.buildId },
7676
data: trimStrings(
7777
{
@@ -162,7 +162,7 @@ export async function release(job: Job<BullMQ.Polling.Release>): Promise<unknown
162162
}
163163
} catch (e) {
164164
job.log(`${e}`);
165-
await prisma.release.update({
165+
await prisma.release.updateMany({
166166
where: { id: job.data.releaseId },
167167
data: { result: Build.Result.Failure, status: Release.Status.Completed }
168168
});

src/lib/server/job-executors/release.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Job } from 'bullmq';
22
import { readFile } from 'fs/promises';
33
import { CodeBuild } from '../aws/codebuild';
4+
import { S3 } from '../aws/s3';
45
import { BullMQ, getQueues } from '../bullmq';
56
import { prisma } from '../prisma';
67
import { Build } from '$lib/server/models/build';
@@ -45,7 +46,7 @@ export async function product(job: Job<BullMQ.Release.Product>): Promise<unknown
4546
)
4647
});
4748
}
48-
const name = `Check status of Release #${release.id}`;
49+
const name = pollName(release.id);
4950
await getQueues().Polling.upsertJobScheduler(name, BullMQ.RepeatEveryMinute, {
5051
name,
5152
data: {
@@ -71,3 +72,24 @@ export async function product(job: Job<BullMQ.Release.Product>): Promise<unknown
7172
});
7273
}
7374
}
75+
76+
export async function cancel(job: Job<BullMQ.Release.Cancel>): Promise<unknown> {
77+
const pollRemoved = await getQueues().Polling.removeJobScheduler(pollName(job.data.release.id));
78+
job.updateProgress(10);
79+
const codeBuild = new CodeBuild();
80+
job.updateProgress(20);
81+
const release = await codeBuild.cancelBuild(
82+
job.data.guid,
83+
CodeBuild.getCodeBuildProjectName('publish_app')
84+
);
85+
job.updateProgress(50);
86+
const s3 = new S3();
87+
job.updateProgress(60);
88+
const objects = await s3.removeCodeBuildFolder(job.data.release);
89+
job.updateProgress(100);
90+
return { pollRemoved, release, objects };
91+
}
92+
93+
function pollName(id: number) {
94+
return `Check status of Release #${id}`;
95+
}

src/routes/(api)/job/[jobId=idNumber]/build/[buildId=idNumber]/+server.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,35 @@ export const PUT: RequestHandler = async ({ request, params }) => {
131131
};
132132

133133
// DELETE /job/[id]/build/[id]
134-
export const DELETE: RequestHandler = async () => {
135-
return ErrorResponse(405, 'DELETE /job/[id]/build/[id] is not supported at this time', {
136-
Allow: 'GET'
134+
export const DELETE: RequestHandler = async ({ params }) => {
135+
const build = await prisma.build.findUnique({
136+
where: {
137+
id: Number(params.buildId)
138+
},
139+
select: {
140+
id: true,
141+
status: true,
142+
build_guid: true,
143+
job: {
144+
select: {
145+
id: true,
146+
app_id: true
147+
}
148+
}
149+
}
137150
});
151+
152+
if (!build) return ErrorResponse(404, 'Build not found');
153+
154+
await prisma.build.delete({ where: { id: build.id } });
155+
156+
if (build.build_guid && build.status !== Build.Status.Completed) {
157+
await getQueues().Builds.add(`Cancel Build #${build.id}`, {
158+
type: BullMQ.JobType.Build_Cancel,
159+
guid: build.build_guid,
160+
build
161+
});
162+
}
163+
164+
return new Response(JSON.stringify({}));
138165
};

src/routes/(api)/job/[jobId=idNumber]/build/[buildId=idNumber]/release/[releaseId=idNumber]/+server.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { RequestHandler } from './$types';
2+
import { BullMQ, getQueues } from '$lib/server/bullmq';
23
import { Release } from '$lib/server/models/release';
34
import { prisma } from '$lib/server/prisma';
45
import { ErrorResponse } from '$lib/utils';
@@ -54,10 +55,39 @@ export const GET: RequestHandler = async ({ params }) => {
5455
};
5556

5657
// DELETE /job/[id]/build/[id]/release/[id]
57-
export const DELETE: RequestHandler = async () => {
58-
return ErrorResponse(
59-
405,
60-
'DELETE /job/[id]/build/[id]/release/[id] is not supported at this time',
61-
{ Allow: 'GET' }
62-
);
58+
export const DELETE: RequestHandler = async ({ params }) => {
59+
const release = await prisma.release.findUnique({
60+
where: {
61+
id: Number(params.releaseId)
62+
},
63+
select: {
64+
id: true,
65+
status: true,
66+
build_guid: true,
67+
build: {
68+
select: {
69+
job: {
70+
select: {
71+
id: true,
72+
app_id: true
73+
}
74+
}
75+
}
76+
}
77+
}
78+
});
79+
80+
if (!release) return ErrorResponse(404, 'Release not found');
81+
82+
await prisma.build.delete({ where: { id: release.id } });
83+
84+
if (release.build_guid && release.status !== Release.Status.Completed) {
85+
await getQueues().Releases.add(`Cancel Release #${release.id}`, {
86+
type: BullMQ.JobType.Release_Cancel,
87+
guid: release.build_guid,
88+
release
89+
});
90+
}
91+
92+
return new Response(JSON.stringify({}));
6393
};

0 commit comments

Comments
 (0)