This commit is contained in:
+143
-8
@@ -305,28 +305,159 @@ export class AppsService {
|
|||||||
throw new BadRequestException('앱 릴리즈 대상 브랜치는 release만 사용할 수 있습니다.');
|
throw new BadRequestException('앱 릴리즈 대상 브랜치는 release만 사용할 수 있습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.promoteReleaseBranches(buildJobs, 'develop', branch);
|
|
||||||
|
|
||||||
const commitShas = [...new Set(buildJobs.map((job) => job.commitSha).filter(Boolean))];
|
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({
|
const release = await this.prisma.appRelease.create({
|
||||||
data: {
|
data: {
|
||||||
appId: body.appId,
|
appId: body.appId,
|
||||||
version: body.version.trim(),
|
version: body.version.trim(),
|
||||||
branch,
|
branch,
|
||||||
commitSha: commitShas.length === 1 ? commitShas[0] : null,
|
commitSha: commitShas.length === 1 ? commitShas[0] : null,
|
||||||
artifact,
|
artifact: null,
|
||||||
imageTags,
|
imageTags: {
|
||||||
status: 'SUCCEEDED',
|
promotion: {
|
||||||
|
sourceBranch: 'develop',
|
||||||
|
targetBranch: branch,
|
||||||
|
sourceBuildJobIds: buildJobs.map((job) => job.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: 'PENDING',
|
||||||
},
|
},
|
||||||
include: { app: true },
|
include: { app: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void this.finalizeReleaseFromBuilds(release.id, buildJobs.map((job) => job.id));
|
||||||
|
|
||||||
return { release };
|
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(
|
private async promoteReleaseBranches(
|
||||||
buildJobs: Array<{
|
buildJobs: Array<{
|
||||||
@@ -2624,3 +2755,7 @@ function getBuildPlanLogs(input: {
|
|||||||
|
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function delay(ms: number) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user