This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "app_releases" ADD COLUMN "logs" TEXT;
|
||||
|
||||
COMMENT ON COLUMN "app_releases"."logs" IS '릴리즈 생성 및 release 빌드 로그';
|
||||
@@ -221,6 +221,7 @@ model AppRelease {
|
||||
artifact String?
|
||||
imageTags Json? @map("image_tags")
|
||||
status JobStatus @default(PENDING)
|
||||
logs String?
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
|
||||
|
||||
+91
-16
@@ -305,25 +305,64 @@ export class AppsService {
|
||||
throw new BadRequestException('앱 릴리즈 대상 브랜치는 release만 사용할 수 있습니다.');
|
||||
}
|
||||
|
||||
const commitShas = [...new Set(buildJobs.map((job) => job.commitSha).filter(Boolean))];
|
||||
const release = await this.prisma.appRelease.create({
|
||||
data: {
|
||||
appId: body.appId,
|
||||
version: body.version.trim(),
|
||||
branch,
|
||||
commitSha: commitShas.length === 1 ? commitShas[0] : null,
|
||||
artifact: null,
|
||||
imageTags: {
|
||||
promotion: {
|
||||
sourceBranch: 'develop',
|
||||
targetBranch: branch,
|
||||
sourceBuildJobIds: buildJobs.map((job) => job.id),
|
||||
},
|
||||
const version = body.version.trim();
|
||||
if (!version) {
|
||||
throw new BadRequestException('릴리즈 버전을 입력해야 합니다.');
|
||||
}
|
||||
|
||||
const existingRelease = await this.prisma.appRelease.findUnique({
|
||||
where: {
|
||||
appId_version: {
|
||||
appId: body.appId,
|
||||
version,
|
||||
},
|
||||
status: 'PENDING',
|
||||
},
|
||||
include: { app: true },
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
},
|
||||
});
|
||||
if (existingRelease && !['FAILED', 'CANCELED'].includes(existingRelease.status)) {
|
||||
throw new BadRequestException(
|
||||
`이미 같은 버전의 릴리즈가 ${existingRelease.status} 상태로 존재합니다. 새 버전을 입력하세요.`,
|
||||
);
|
||||
}
|
||||
|
||||
const commitShas = [...new Set(buildJobs.map((job) => job.commitSha).filter(Boolean))];
|
||||
const releaseLogs = [
|
||||
`[queued] release creation: ${app.name} ${version}`,
|
||||
`[source] develop build jobs: ${buildJobs.map((job) => job.id).join(', ')}`,
|
||||
`[target] branch ${branch}`,
|
||||
'[pending] waiting for release worker',
|
||||
];
|
||||
const releaseData = {
|
||||
branch,
|
||||
commitSha: commitShas.length === 1 ? commitShas[0] : null,
|
||||
artifact: null,
|
||||
imageTags: {
|
||||
promotion: {
|
||||
sourceBranch: 'develop',
|
||||
targetBranch: branch,
|
||||
sourceBuildJobIds: buildJobs.map((job) => job.id),
|
||||
},
|
||||
},
|
||||
status: 'PENDING' as const,
|
||||
logs: releaseLogs.join('\n'),
|
||||
};
|
||||
const release = existingRelease
|
||||
? await this.prisma.appRelease.update({
|
||||
where: { id: existingRelease.id },
|
||||
data: releaseData,
|
||||
include: { app: true },
|
||||
})
|
||||
: await this.prisma.appRelease.create({
|
||||
data: {
|
||||
appId: body.appId,
|
||||
version,
|
||||
...releaseData,
|
||||
},
|
||||
include: { app: true },
|
||||
});
|
||||
|
||||
void this.finalizeReleaseFromBuilds(release.id, buildJobs.map((job) => job.id));
|
||||
|
||||
@@ -332,6 +371,7 @@ export class AppsService {
|
||||
|
||||
private async finalizeReleaseFromBuilds(releaseId: string, sourceBuildJobIds: string[]) {
|
||||
try {
|
||||
await this.appendReleaseLog(releaseId, '[running] release worker started');
|
||||
const release = await this.prisma.appRelease.findUnique({
|
||||
where: { id: releaseId },
|
||||
include: {
|
||||
@@ -342,6 +382,7 @@ export class AppsService {
|
||||
});
|
||||
if (!release) return;
|
||||
|
||||
await this.appendReleaseLog(releaseId, `[target] ${release.app.name} ${release.version} (${release.branch})`);
|
||||
const sourceBuildJobs = await this.prisma.buildJob.findMany({
|
||||
where: { id: { in: sourceBuildJobIds } },
|
||||
include: { component: { include: { app: true } } },
|
||||
@@ -351,18 +392,30 @@ export class AppsService {
|
||||
throw new Error('릴리즈 기준 빌드 작업을 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
for (const job of sourceBuildJobs) {
|
||||
await this.appendReleaseLog(releaseId, `[source] ${job.component.name} develop ${job.commitSha?.slice(0, 7) ?? '-'}`);
|
||||
}
|
||||
await this.appendReleaseLog(releaseId, `[promote] develop -> ${release.branch}`);
|
||||
await this.promoteReleaseBranches(sourceBuildJobs, 'develop', release.branch);
|
||||
await this.appendReleaseLog(releaseId, '[promote] completed');
|
||||
|
||||
const releaseBuildJobs = await this.createReleaseBuildJobs(sourceBuildJobs, release.branch);
|
||||
for (const job of releaseBuildJobs) {
|
||||
await this.appendReleaseLog(releaseId, `[build] queued ${job.component.name} ${job.branch} ${job.commitSha?.slice(0, 7) ?? '-'}`);
|
||||
}
|
||||
await this.appendReleaseLog(releaseId, '[wait] waiting for release build jobs');
|
||||
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}`);
|
||||
}
|
||||
await this.appendReleaseLog(releaseId, '[build] release build jobs completed');
|
||||
|
||||
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.appendReleaseLog(releaseId, '[done] release completed');
|
||||
await this.prisma.appRelease.update({
|
||||
where: { id: release.id },
|
||||
data: {
|
||||
@@ -374,16 +427,34 @@ export class AppsService {
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown release build failure';
|
||||
const release = await this.prisma.appRelease.findUnique({
|
||||
where: { id: releaseId },
|
||||
select: { logs: true },
|
||||
}).catch(() => null);
|
||||
await this.prisma.appRelease.update({
|
||||
where: { id: releaseId },
|
||||
data: {
|
||||
status: 'FAILED',
|
||||
imageTags: { error: message },
|
||||
logs: truncateReleaseLogs(appendLog(release?.logs ?? null, `[failed] ${message}`)),
|
||||
},
|
||||
}).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
private async appendReleaseLog(releaseId: string, line: string) {
|
||||
const release = await this.prisma.appRelease.findUnique({
|
||||
where: { id: releaseId },
|
||||
select: { logs: true },
|
||||
});
|
||||
if (!release) return;
|
||||
|
||||
await this.prisma.appRelease.update({
|
||||
where: { id: releaseId },
|
||||
data: { logs: truncateReleaseLogs(appendLog(release.logs, line)) },
|
||||
});
|
||||
}
|
||||
|
||||
private async createReleaseBuildJobs(
|
||||
sourceBuildJobs: Array<{
|
||||
componentId: string;
|
||||
@@ -1847,6 +1918,10 @@ function appendLog(logs: string | null, line: string) {
|
||||
return [logs, line].filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
function truncateReleaseLogs(logs: string) {
|
||||
return logs.length > 200_000 ? logs.slice(-200_000) : logs;
|
||||
}
|
||||
|
||||
function sanitizeK8sName(value: string) {
|
||||
return value
|
||||
.trim()
|
||||
|
||||
Reference in New Issue
Block a user