This commit is contained in:
+143
-8
@@ -305,28 +305,159 @@ export class AppsService {
|
||||
throw new BadRequestException('앱 릴리즈 대상 브랜치는 release만 사용할 수 있습니다.');
|
||||
}
|
||||
|
||||
await this.promoteReleaseBranches(buildJobs, 'develop', branch);
|
||||
|
||||
const commitShas = [...new Set(buildJobs.map((job) => job.commitSha).filter(Boolean))];
|
||||
const artifact = buildJobs.find((job) => job.artifact)?.artifact ?? null;
|
||||
const imageTags = createReleaseImageTags(buildJobs, app.components);
|
||||
|
||||
const release = await this.prisma.appRelease.create({
|
||||
data: {
|
||||
appId: body.appId,
|
||||
version: body.version.trim(),
|
||||
branch,
|
||||
commitSha: commitShas.length === 1 ? commitShas[0] : null,
|
||||
artifact,
|
||||
imageTags,
|
||||
status: 'SUCCEEDED',
|
||||
artifact: null,
|
||||
imageTags: {
|
||||
promotion: {
|
||||
sourceBranch: 'develop',
|
||||
targetBranch: branch,
|
||||
sourceBuildJobIds: buildJobs.map((job) => job.id),
|
||||
},
|
||||
},
|
||||
status: 'PENDING',
|
||||
},
|
||||
include: { app: true },
|
||||
});
|
||||
|
||||
void this.finalizeReleaseFromBuilds(release.id, buildJobs.map((job) => job.id));
|
||||
|
||||
return { release };
|
||||
}
|
||||
|
||||
private async finalizeReleaseFromBuilds(releaseId: string, sourceBuildJobIds: string[]) {
|
||||
try {
|
||||
const release = await this.prisma.appRelease.findUnique({
|
||||
where: { id: releaseId },
|
||||
include: {
|
||||
app: {
|
||||
include: { components: { orderBy: { name: 'asc' } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!release) return;
|
||||
|
||||
const sourceBuildJobs = await this.prisma.buildJob.findMany({
|
||||
where: { id: { in: sourceBuildJobIds } },
|
||||
include: { component: { include: { app: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
if (sourceBuildJobs.length !== sourceBuildJobIds.length) {
|
||||
throw new Error('릴리즈 기준 빌드 작업을 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
await this.promoteReleaseBranches(sourceBuildJobs, 'develop', release.branch);
|
||||
const releaseBuildJobs = await this.createReleaseBuildJobs(sourceBuildJobs, release.branch);
|
||||
const completedBuildJobs = await this.waitForBuildJobs(releaseBuildJobs.map((job) => job.id));
|
||||
const failed = completedBuildJobs.find((job) => job.status !== 'SUCCEEDED');
|
||||
if (failed) {
|
||||
throw new Error(`${failed.component.name} release 빌드 실패: ${failed.status}`);
|
||||
}
|
||||
|
||||
const commitShas = [...new Set(completedBuildJobs.map((job) => job.commitSha).filter(Boolean))];
|
||||
const artifact = completedBuildJobs.find((job) => job.artifact)?.artifact ?? null;
|
||||
const imageTags = createReleaseImageTags(completedBuildJobs, release.app.components);
|
||||
|
||||
await this.prisma.appRelease.update({
|
||||
where: { id: release.id },
|
||||
data: {
|
||||
commitSha: commitShas.length === 1 ? commitShas[0] : release.commitSha,
|
||||
artifact,
|
||||
imageTags,
|
||||
status: 'SUCCEEDED',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown release build failure';
|
||||
await this.prisma.appRelease.update({
|
||||
where: { id: releaseId },
|
||||
data: {
|
||||
status: 'FAILED',
|
||||
imageTags: { error: message },
|
||||
},
|
||||
}).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
private async createReleaseBuildJobs(
|
||||
sourceBuildJobs: Array<{
|
||||
componentId: string;
|
||||
commitSha: string | null;
|
||||
component: {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
repository: string | null;
|
||||
buildTarget: string | null;
|
||||
app: { name: string };
|
||||
};
|
||||
}>,
|
||||
branch: string,
|
||||
) {
|
||||
const jobs = [];
|
||||
for (const sourceJob of sourceBuildJobs) {
|
||||
const component = sourceJob.component;
|
||||
const artifact = getBuildArtifact(component, branch, sourceJob.commitSha);
|
||||
const image = getBuildImage(
|
||||
component,
|
||||
branch,
|
||||
sourceJob.commitSha,
|
||||
this.configService.get<string>('BUILD_REGISTRY', 'registry.local'),
|
||||
);
|
||||
const job = await this.prisma.buildJob.create({
|
||||
data: {
|
||||
componentId: component.id,
|
||||
branch,
|
||||
commitSha: sourceJob.commitSha,
|
||||
artifact,
|
||||
image,
|
||||
status: 'PENDING',
|
||||
logs: getBuildPlanLogs({
|
||||
appName: component.app.name,
|
||||
componentName: component.name,
|
||||
componentType: component.type,
|
||||
repository: component.repository || '',
|
||||
branch,
|
||||
commitSha: sourceJob.commitSha,
|
||||
buildTarget: component.buildTarget,
|
||||
artifact,
|
||||
image,
|
||||
giteaBaseUrl: this.configService.get<string>(
|
||||
'GITEA_BASE_URL',
|
||||
'http://localhost:3000',
|
||||
),
|
||||
}).join('\n'),
|
||||
},
|
||||
include: { component: true },
|
||||
});
|
||||
jobs.push(job);
|
||||
}
|
||||
return jobs;
|
||||
}
|
||||
|
||||
private async waitForBuildJobs(buildJobIds: string[]) {
|
||||
const timeoutMs = Number(this.configService.get<string>('RELEASE_BUILD_TIMEOUT_MS', '1800000'));
|
||||
const startedAt = Date.now();
|
||||
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const jobs = await this.prisma.buildJob.findMany({
|
||||
where: { id: { in: buildJobIds } },
|
||||
include: { component: true },
|
||||
});
|
||||
if (jobs.length === buildJobIds.length && jobs.every((job) => ['SUCCEEDED', 'FAILED', 'CANCELED'].includes(job.status))) {
|
||||
return jobs;
|
||||
}
|
||||
await delay(3000);
|
||||
}
|
||||
|
||||
throw new Error('release 빌드 완료 대기 시간이 초과되었습니다.');
|
||||
}
|
||||
|
||||
|
||||
private async promoteReleaseBranches(
|
||||
buildJobs: Array<{
|
||||
@@ -2624,3 +2755,7 @@ function getBuildPlanLogs(input: {
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
function delay(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user