support launcher backend source and image builds

This commit is contained in:
2026-07-02 14:48:25 +09:00
parent 3d15bdf6bb
commit 3195d36527
2 changed files with 156 additions and 16 deletions
+12
View File
@@ -33,6 +33,10 @@ export class CreateLauncherVersionDto {
} }
export class UpdateLauncherSourceDto { export class UpdateLauncherSourceDto {
@IsOptional()
@IsIn(['launcher-client', 'launcher-backend'])
target?: 'launcher-client' | 'launcher-backend';
@IsString() @IsString()
repository!: string; repository!: string;
@@ -43,6 +47,14 @@ export class UpdateLauncherSourceDto {
@IsOptional() @IsOptional()
@IsString() @IsString()
commitSha?: string; commitSha?: string;
@IsOptional()
@IsString()
image?: string;
@IsOptional()
@IsString()
dockerfile?: string;
} }
export class LinkLauncherVersionSourceDto { export class LinkLauncherVersionSourceDto {
+144 -16
View File
@@ -23,9 +23,14 @@ import {
const execAsync = promisify(exec); const execAsync = promisify(exec);
const LAUNCHER_SOURCE_SETTING_KEYS = { const LAUNCHER_SOURCE_SETTING_KEYS = {
repository: 'launcher.source.repository', repository: 'launcher.client.source.repository',
branch: 'launcher.source.branch', branch: 'launcher.client.source.branch',
commitSha: 'launcher.source.commit_sha', commitSha: 'launcher.client.source.commit_sha',
backendRepository: 'launcher.backend.source.repository',
backendBranch: 'launcher.backend.source.branch',
backendCommitSha: 'launcher.backend.source.commit_sha',
backendImage: 'launcher.backend.image',
backendDockerfile: 'launcher.backend.dockerfile',
} as const; } as const;
@Injectable() @Injectable()
@@ -101,11 +106,51 @@ export class LauncherService {
} }
async updateSource(body: UpdateLauncherSourceDto) { async updateSource(body: UpdateLauncherSourceDto) {
const target = body.target ?? 'launcher-client';
const repository = normalizeRepository(body.repository); const repository = normalizeRepository(body.repository);
const branch = body.branch?.trim() || 'release'; const branch = body.branch?.trim() || 'release';
const commitSha = body.commitSha?.trim() || ''; const commitSha = body.commitSha?.trim() || '';
const isBackend = target === 'launcher-backend';
const image = body.image?.trim() || this.configService.get<string>('LAUNCHER_BACKEND_IMAGE', 'localhost:5000/studio-launcher-be');
const dockerfile = body.dockerfile?.trim() || 'Dockerfile';
await this.launcherPrisma.$transaction([ const writes = isBackend ? [
this.upsertCommonSettingByKey(
LAUNCHER_SOURCE_SETTING_KEYS.backendRepository,
'런처 백엔드 소스 저장소',
'source',
repository,
'런처 백엔드 컨테이너 빌드에 사용할 저장소',
),
this.upsertCommonSettingByKey(
LAUNCHER_SOURCE_SETTING_KEYS.backendBranch,
'런처 백엔드 소스 브랜치',
'source',
branch,
'런처 백엔드 컨테이너 빌드 기준 브랜치',
),
this.upsertCommonSettingByKey(
LAUNCHER_SOURCE_SETTING_KEYS.backendCommitSha,
'런처 백엔드 소스 커밋',
'source',
commitSha,
'비워두면 브랜치 최신 커밋 기준',
),
this.upsertCommonSettingByKey(
LAUNCHER_SOURCE_SETTING_KEYS.backendImage,
'런처 백엔드 이미지',
'source',
image,
'런처 백엔드 컨테이너 이미지 이름',
),
this.upsertCommonSettingByKey(
LAUNCHER_SOURCE_SETTING_KEYS.backendDockerfile,
'런처 백엔드 Dockerfile',
'source',
dockerfile,
'런처 백엔드 빌드에 사용할 Dockerfile 경로',
),
] : [
this.upsertCommonSettingByKey( this.upsertCommonSettingByKey(
LAUNCHER_SOURCE_SETTING_KEYS.repository, LAUNCHER_SOURCE_SETTING_KEYS.repository,
'런처 소스 저장소', '런처 소스 저장소',
@@ -127,9 +172,19 @@ export class LauncherService {
commitSha, commitSha,
'비워두면 브랜치 최신 커밋 기준', '비워두면 브랜치 최신 커밋 기준',
), ),
]); ];
return { source: { repository, branch, commitSha: commitSha || null } }; await this.launcherPrisma.$transaction(writes);
return {
source: {
repository,
branch,
commitSha: commitSha || null,
image: isBackend ? image : undefined,
dockerfile: isBackend ? dockerfile : undefined,
},
};
} }
private async getSourceSettings() { private async getSourceSettings() {
@@ -140,6 +195,11 @@ export class LauncherService {
LAUNCHER_SOURCE_SETTING_KEYS.repository, LAUNCHER_SOURCE_SETTING_KEYS.repository,
LAUNCHER_SOURCE_SETTING_KEYS.branch, LAUNCHER_SOURCE_SETTING_KEYS.branch,
LAUNCHER_SOURCE_SETTING_KEYS.commitSha, LAUNCHER_SOURCE_SETTING_KEYS.commitSha,
LAUNCHER_SOURCE_SETTING_KEYS.backendRepository,
LAUNCHER_SOURCE_SETTING_KEYS.backendBranch,
LAUNCHER_SOURCE_SETTING_KEYS.backendCommitSha,
LAUNCHER_SOURCE_SETTING_KEYS.backendImage,
LAUNCHER_SOURCE_SETTING_KEYS.backendDockerfile,
], ],
}, },
}, },
@@ -148,11 +208,28 @@ export class LauncherService {
const repository = byKey.get(LAUNCHER_SOURCE_SETTING_KEYS.repository) || null; const repository = byKey.get(LAUNCHER_SOURCE_SETTING_KEYS.repository) || null;
const branch = byKey.get(LAUNCHER_SOURCE_SETTING_KEYS.branch) || 'release'; const branch = byKey.get(LAUNCHER_SOURCE_SETTING_KEYS.branch) || 'release';
const commitSha = byKey.get(LAUNCHER_SOURCE_SETTING_KEYS.commitSha) || null; const commitSha = byKey.get(LAUNCHER_SOURCE_SETTING_KEYS.commitSha) || null;
const backendRepository = byKey.get(LAUNCHER_SOURCE_SETTING_KEYS.backendRepository) || null;
const backendBranch = byKey.get(LAUNCHER_SOURCE_SETTING_KEYS.backendBranch) || 'release';
const backendCommitSha = byKey.get(LAUNCHER_SOURCE_SETTING_KEYS.backendCommitSha) || null;
const backendImage = byKey.get(LAUNCHER_SOURCE_SETTING_KEYS.backendImage) || this.configService.get<string>('LAUNCHER_BACKEND_IMAGE', 'localhost:5000/studio-launcher-be');
const backendDockerfile = byKey.get(LAUNCHER_SOURCE_SETTING_KEYS.backendDockerfile) || 'Dockerfile';
return { return {
repository, repository,
branch, branch,
commitSha, commitSha,
client: {
repository,
branch,
commitSha,
},
backend: {
repository: backendRepository,
branch: backendBranch,
commitSha: backendCommitSha,
image: backendImage,
dockerfile: backendDockerfile,
},
}; };
} }
@@ -690,27 +767,37 @@ export class LauncherService {
if (!body.version?.trim()) throw new BadRequestException('버전을 입력하세요.'); if (!body.version?.trim()) throw new BadRequestException('버전을 입력하세요.');
if (!body.artifact?.trim()) throw new BadRequestException('산출물 파일명을 입력하세요.'); if (!body.artifact?.trim()) throw new BadRequestException('산출물 파일명을 입력하세요.');
const source = await this.getSourceSettings(); const source = await this.getSourceSettings();
const repository = body.repository ? normalizeRepository(body.repository) : source.repository; const target = body.target?.trim() || 'launcher-client';
const branch = body.branch?.trim() || source.branch || 'release'; const isBackend = target === 'launcher-backend';
const commitSha = body.commitSha?.trim() || source.commitSha || null; const repository = body.repository
const plan = this.createLauncherArtifactPlan(body.version.trim(), branch, commitSha, body.artifact.trim(), body.uploadPath?.trim(), body.downloadUrl?.trim()); ? normalizeRepository(body.repository)
if (!plan.uploadPath) throw new BadRequestException('업로드 경로를 입력하세요.'); : isBackend
if (!plan.downloadUrl) throw new BadRequestException('다운로드 URL을 입력하세요.'); ? source.backend.repository
: source.repository;
const branch = body.branch?.trim() || (isBackend ? source.backend.branch : source.branch) || 'release';
const commitSha = body.commitSha?.trim() || (isBackend ? source.backend.commitSha : source.commitSha) || null;
const plan = isBackend
? this.createLauncherBackendImagePlan(body.version.trim(), body.artifact.trim(), body.uploadPath?.trim(), body.downloadUrl?.trim())
: this.createLauncherArtifactPlan(body.version.trim(), branch, commitSha, body.artifact.trim(), body.uploadPath?.trim(), body.downloadUrl?.trim());
if (!repository) throw new BadRequestException('소스 저장소가 연결되지 않았습니다.');
if (!plan.uploadPath) throw new BadRequestException(isBackend ? 'Dockerfile 경로를 입력하세요.' : '업로드 경로를 입력하세요.');
if (!plan.downloadUrl) throw new BadRequestException(isBackend ? '이미지 태그를 입력하세요.' : '다운로드 URL을 입력하세요.');
const logs = [ const logs = [
`[queued] launcher build created: ${body.artifact.trim()}`, `[queued] launcher build created: ${body.artifact.trim()}`,
`[target] ${target}`,
repository ? `[plan] clone ${this.toCloneUrl(repository)}` : '[plan] source repository is not linked', repository ? `[plan] clone ${this.toCloneUrl(repository)}` : '[plan] source repository is not linked',
`[plan] checkout branch ${branch}`, `[plan] checkout branch ${branch}`,
commitSha ? `[plan] checkout commit ${commitSha}` : '[plan] use branch latest commit', commitSha ? `[plan] checkout commit ${commitSha}` : '[plan] use branch latest commit',
`[plan] build deb package`, isBackend ? `[plan] build and push image ${plan.downloadUrl}` : `[plan] build deb package`,
`[plan] upload ${body.artifact.trim()} to ${plan.uploadPath}`, isBackend ? `[plan] dockerfile ${plan.uploadPath}` : `[plan] upload ${body.artifact.trim()} to ${plan.uploadPath}`,
`[plan] publish download url ${plan.downloadUrl}`, isBackend ? `[plan] image tag ${plan.downloadUrl}` : `[plan] publish download url ${plan.downloadUrl}`,
'[pending] waiting for launcher build runner', '[pending] waiting for launcher build runner',
].join('\n'); ].join('\n');
const job = await this.prisma.launcherBuildJob.create({ const job = await this.prisma.launcherBuildJob.create({
data: { data: {
target: body.target?.trim() || 'launcher-client', target,
repository, repository,
branch, branch,
commitSha, commitSha,
@@ -783,6 +870,30 @@ export class LauncherService {
await runCommand(`git checkout ${quote(job.commitSha)}`, workDir, append); await runCommand(`git checkout ${quote(job.commitSha)}`, workDir, append);
} }
if (job.target === 'launcher-backend') {
const dockerfile = job.uploadPath || 'Dockerfile';
const imageTag = job.downloadUrl || job.artifact;
await append(`[backend] dockerfile ${dockerfile}`);
await append(`[backend] image ${imageTag}`);
await runCommand(
`docker build -t ${quote(imageTag)} -f ${quote(dockerfile)} .`,
workDir,
append,
);
await runCommand(`docker push ${quote(imageTag)}`, workDir, append);
await append('[done] launcher backend image build completed');
await this.prisma.launcherBuildJob.update({
where: { id: jobId },
data: {
status: 'SUCCEEDED',
finishedAt: new Date(),
logs,
},
});
return;
}
const buildCommand = process.env.LAUNCHER_BUILD_COMMAND || 'npm ci && npm run build:linux'; const buildCommand = process.env.LAUNCHER_BUILD_COMMAND || 'npm ci && npm run build:linux';
await runCommand(buildCommand, workDir, append); await runCommand(buildCommand, workDir, append);
@@ -848,6 +959,23 @@ export class LauncherService {
}; };
} }
private createLauncherBackendImagePlan(
version: string,
artifact: string,
dockerfile?: string,
imageTag?: string,
) {
const registry = this.configService.get<string>('BUILD_REGISTRY', 'localhost:5000').replace(/\/$/, '');
const imageName = artifact.includes(':')
? artifact
: `${registry}/${artifact.replace(/^\/+/, '')}:${version}`;
return {
uploadPath: dockerfile || 'Dockerfile',
downloadUrl: imageTag || imageName,
};
}
private async uploadLauncherArtifactIfConfigured( private async uploadLauncherArtifactIfConfigured(
job: { job: {
version: string; version: string;