feat: add quick deploy for launcher backend builds
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-15 16:01:09 +09:00
parent 53b4909df7
commit 3945e42c53
2 changed files with 68 additions and 23 deletions
+54 -14
View File
@@ -954,7 +954,12 @@ export class AppsService {
async createDeploymentJob(body: CreateDeploymentJobDto, requester?: AdminRequester) { async createDeploymentJob(body: CreateDeploymentJobDto, requester?: AdminRequester) {
const namespace = normalizeNamespace(body.namespace); const namespace = normalizeNamespace(body.namespace);
const environment = body.environment.trim(); const environment = body.environment.trim();
const release = await this.prisma.appRelease.findUnique({ if (body.releaseId && body.buildJobId) {
throw new BadRequestException('릴리즈 또는 빌드 중 하나만 배포 대상으로 선택할 수 있습니다.');
}
const release = body.releaseId
? await this.prisma.appRelease.findUnique({
where: { id: body.releaseId }, where: { id: body.releaseId },
include: { include: {
app: { app: {
@@ -963,22 +968,56 @@ export class AppsService {
}, },
}, },
}, },
}); })
: null;
const quickBuild = body.buildJobId
? await this.prisma.buildJob.findUnique({
where: { id: body.buildJobId },
include: {
component: {
include: {
app: {
include: { components: true },
},
},
},
},
})
: null;
if (!release) { if (body.releaseId && !release) {
throw new NotFoundException('릴리즈를 찾을 수 없습니다.'); throw new NotFoundException('릴리즈를 찾을 수 없습니다.');
} }
if (body.buildJobId && !quickBuild) {
if (release.status !== 'SUCCEEDED') { throw new NotFoundException('빌드 작업을 찾을 수 없습니다.');
}
if (!release && !quickBuild) {
throw new BadRequestException('배포할 릴리즈 또는 빌드를 선택하세요.');
}
if (release && release.status !== 'SUCCEEDED') {
throw new BadRequestException('성공한 릴리즈만 배포할 수 있습니다.'); throw new BadRequestException('성공한 릴리즈만 배포할 수 있습니다.');
} }
if (quickBuild && quickBuild.status !== 'SUCCEEDED') {
throw new BadRequestException('성공한 빌드만 빠른 배포할 수 있습니다.');
}
if (quickBuild && !['CONTAINER', 'STATIC_WEB'].includes(quickBuild.component.type)) {
throw new BadRequestException('컨테이너 이미지 빌드만 빠른 배포할 수 있습니다.');
}
if (quickBuild && !quickBuild.image) {
throw new BadRequestException('배포할 컨테이너 이미지가 없습니다.');
}
const imageTags = parseReleaseImageTags(release.imageTags); const app = release?.app ?? quickBuild!.component.app;
const images = imageTags.images; const images = release
? parseReleaseImageTags(release.imageTags).images
: { [quickBuild!.component.name]: quickBuild!.image! };
const deploymentLabel = release
? `${app.name} ${release.version}`
: `${app.name} ${quickBuild!.component.name} ${quickBuild!.commitSha?.slice(0, 7) ?? 'quick'}`;
const requestedResources = body.resources; const requestedResources = body.resources;
const runtimeProfile = await this.prisma.runtimeEnvProfile.findFirst({ const runtimeProfile = await this.prisma.runtimeEnvProfile.findFirst({
where: { where: {
appId: release.appId, appId: app.id,
environment, environment,
namespace, namespace,
}, },
@@ -1002,8 +1041,8 @@ export class AppsService {
const deploymentJob = await this.prisma.deploymentJob.create({ const deploymentJob = await this.prisma.deploymentJob.create({
data: { data: {
appId: release.appId, appId: app.id,
releaseId: release.id, releaseId: release?.id,
environment, environment,
namespace, namespace,
status: 'RUNNING', status: 'RUNNING',
@@ -1011,7 +1050,8 @@ export class AppsService {
...this.requesterData(requester), ...this.requesterData(requester),
startedAt: new Date(), startedAt: new Date(),
logs: [ logs: [
`[running] deployment started: ${release.app.name} ${release.version}`, `[running] deployment started: ${deploymentLabel}`,
...(quickBuild ? [`[source] quick build ${quickBuild.id} ${quickBuild.image}`] : []),
`[target] environment=${environment} namespace=${namespace}`, `[target] environment=${environment} namespace=${namespace}`,
].join('\n'), ].join('\n'),
}, },
@@ -1026,7 +1066,7 @@ export class AppsService {
for (const [componentName, image] of Object.entries(images)) { for (const [componentName, image] of Object.entries(images)) {
const deploymentName = sanitizeK8sName(componentName); const deploymentName = sanitizeK8sName(componentName);
const component = release.app.components.find((item) => item.name === componentName); const component = app.components.find((item) => item.name === componentName);
const workloadType = normalizeWorkloadType(component?.workloadType, component?.type); const workloadType = normalizeWorkloadType(component?.workloadType, component?.type);
const deploymentExists = workloadType === 'StatefulSet' const deploymentExists = workloadType === 'StatefulSet'
? await this.statefulSetExists(namespace, deploymentName) ? await this.statefulSetExists(namespace, deploymentName)
@@ -1173,7 +1213,7 @@ export class AppsService {
} }
} }
if (body.updateLauncherAppVersion !== false) { if (release && body.updateLauncherAppVersion !== false) {
try { try {
await this.launcherService.publishAppReleaseToLauncher(release.id); await this.launcherService.publishAppReleaseToLauncher(release.id);
logs.push('[launcher] app version updated'); logs.push('[launcher] app version updated');
@@ -1182,7 +1222,7 @@ export class AppsService {
logs.push(`[launcher] app version update failed: ${launcherMessage}`); logs.push(`[launcher] app version update failed: ${launcherMessage}`);
} }
} else { } else {
logs.push('[launcher] app version update skipped'); logs.push(quickBuild ? '[launcher] app version update skipped for quick deployment' : '[launcher] app version update skipped');
} }
logs.push('[done] deployment completed'); logs.push('[done] deployment completed');
+6 -1
View File
@@ -220,8 +220,13 @@ export class DeploymentResourceLimitsDto {
} }
export class CreateDeploymentJobDto { export class CreateDeploymentJobDto {
@IsOptional()
@IsUUID() @IsUUID()
releaseId!: string; releaseId?: string;
@IsOptional()
@IsUUID()
buildJobId?: string;
@IsString() @IsString()
environment!: string; environment!: string;