From dd11e5186a8d52837946df3acd076754a3eda4e3 Mon Sep 17 00:00:00 2001 From: chyoo Date: Fri, 3 Jul 2026 10:45:55 +0900 Subject: [PATCH] read launcher build version from package json --- src/launcher/dto.ts | 6 ++- src/launcher/launcher.service.ts | 81 ++++++++++++++++++++++++++------ 2 files changed, 71 insertions(+), 16 deletions(-) diff --git a/src/launcher/dto.ts b/src/launcher/dto.ts index 5e96ba2..cd0c2bd 100644 --- a/src/launcher/dto.ts +++ b/src/launcher/dto.ts @@ -88,11 +88,13 @@ export class CreateLauncherBuildJobDto { @IsString() commitSha?: string; + @IsOptional() @IsString() - version!: string; + version?: string; + @IsOptional() @IsString() - artifact!: string; + artifact?: string; @IsOptional() @IsString() diff --git a/src/launcher/launcher.service.ts b/src/launcher/launcher.service.ts index 3689b37..9d1128b 100644 --- a/src/launcher/launcher.service.ts +++ b/src/launcher/launcher.service.ts @@ -3,7 +3,8 @@ import { ConfigService } from '@nestjs/config'; import { exec } from 'node:child_process'; import { createHash } from 'node:crypto'; import { createReadStream } from 'node:fs'; -import { copyFile, mkdir, rm, stat } from 'node:fs/promises'; +import { copyFile, mkdir, readFile, rm, stat } from 'node:fs/promises'; +import { basename } from 'node:path'; import { promisify } from 'node:util'; import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; @@ -764,11 +765,12 @@ export class LauncherService { } async createBuildJob(body: CreateLauncherBuildJobDto) { - if (!body.version?.trim()) throw new BadRequestException('버전을 입력하세요.'); - if (!body.artifact?.trim()) throw new BadRequestException('산출물 파일명을 입력하세요.'); const source = await this.getSourceSettings(); const target = body.target?.trim() || 'launcher-client'; const isBackend = target === 'launcher-backend'; + if (isBackend && !body.artifact?.trim()) throw new BadRequestException('이미지 이름을 입력하세요.'); + const requestedVersion = body.version?.trim() || 'package-json'; + const requestedArtifact = body.artifact?.trim() || 'autonomy-studio-launcher.deb'; const repository = body.repository ? normalizeRepository(body.repository) : isBackend @@ -777,20 +779,21 @@ export class LauncherService { 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()); + ? this.createLauncherBackendImagePlan(requestedVersion, requestedArtifact, body.uploadPath?.trim(), body.downloadUrl?.trim()) + : this.createLauncherArtifactPlan(requestedVersion, branch, commitSha, requestedArtifact, 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 = [ - `[queued] launcher build created: ${body.artifact.trim()}`, + `[queued] launcher build created: ${requestedArtifact}`, `[target] ${target}`, repository ? `[plan] clone ${this.toCloneUrl(repository)}` : '[plan] source repository is not linked', `[plan] checkout branch ${branch}`, commitSha ? `[plan] checkout commit ${commitSha}` : '[plan] use branch latest commit', isBackend ? `[plan] build and push image ${plan.downloadUrl}` : `[plan] build deb package`, - isBackend ? `[plan] dockerfile ${plan.uploadPath}` : `[plan] upload ${body.artifact.trim()} to ${plan.uploadPath}`, + isBackend ? `[plan] dockerfile ${plan.uploadPath}` : `[plan] upload ${requestedArtifact} to ${plan.uploadPath}`, + '[plan] final version will be read from package.json after clone', isBackend ? `[plan] image tag ${plan.downloadUrl}` : `[plan] publish download url ${plan.downloadUrl}`, '[pending] waiting for launcher build runner', ].join('\n'); @@ -801,8 +804,8 @@ export class LauncherService { repository, branch, commitSha, - version: body.version.trim(), - artifact: body.artifact.trim(), + version: requestedVersion, + artifact: requestedArtifact, uploadPath: plan.uploadPath, downloadUrl: plan.downloadUrl, logs, @@ -871,10 +874,17 @@ export class LauncherService { } if (job.target === 'launcher-backend') { + const packageVersion = (await this.readPackageVersion(workDir)) || job.version; const dockerfile = job.uploadPath || 'Dockerfile'; - const imageTag = job.downloadUrl || job.artifact; + const imagePlan = this.createLauncherBackendImagePlan(packageVersion, job.artifact, dockerfile); + const imageTag = imagePlan.downloadUrl; + await append(`[backend] package version ${packageVersion}`); await append(`[backend] dockerfile ${dockerfile}`); await append(`[backend] image ${imageTag}`); + await this.prisma.launcherBuildJob.update({ + where: { id: jobId }, + data: { version: packageVersion, uploadPath: dockerfile, downloadUrl: imageTag }, + }); await runCommand( `docker build -t ${quote(imageTag)} -f ${quote(dockerfile)} .`, workDir, @@ -888,6 +898,9 @@ export class LauncherService { data: { status: 'SUCCEEDED', finishedAt: new Date(), + version: packageVersion, + uploadPath: dockerfile, + downloadUrl: imageTag, logs, }, }); @@ -901,10 +914,35 @@ export class LauncherService { const debPath = stdout.trim().split('\n').filter(Boolean)[0]; if (!debPath) throw new Error('.deb 산출물을 찾지 못했습니다.'); - const uploadedUrl = await this.uploadLauncherArtifactIfConfigured(job, debPath, append); + const packageVersion = (await this.readPackageVersion(workDir)) || job.version; + const artifactName = basename(debPath); + const artifactPlan = this.createLauncherArtifactPlan(packageVersion, job.branch, job.commitSha, artifactName, job.uploadPath, job.downloadUrl); + const finalJob = { + version: packageVersion, + branch: job.branch, + commitSha: job.commitSha, + artifact: artifactName, + uploadPath: artifactPlan.uploadPath, + }; + await append( + `[artifact] package version ${packageVersion}`, + `[artifact] found ${artifactName}`, + `[artifact] upload key ${artifactPlan.uploadPath}`, + ); + await this.prisma.launcherBuildJob.update({ + where: { id: jobId }, + data: { + version: packageVersion, + artifact: artifactName, + uploadPath: artifactPlan.uploadPath, + downloadUrl: artifactPlan.downloadUrl, + }, + }); + + const uploadedUrl = await this.uploadLauncherArtifactIfConfigured(finalJob, debPath, append); if (!uploadedUrl) { - await mkdir(job.uploadPath, { recursive: true }); - const destination = `${job.uploadPath.replace(/\/$/, '')}/${job.artifact}`; + await mkdir(artifactPlan.uploadPath, { recursive: true }); + const destination = `${artifactPlan.uploadPath.replace(/\/$/, '')}/${artifactName}`; await copyFile(debPath, destination); await append(`[upload] ${destination}`); } @@ -915,8 +953,11 @@ export class LauncherService { data: { status: 'SUCCEEDED', finishedAt: new Date(), + version: packageVersion, + artifact: artifactName, + uploadPath: artifactPlan.uploadPath, + downloadUrl: uploadedUrl || artifactPlan.downloadUrl, logs, - ...(uploadedUrl ? { downloadUrl: uploadedUrl } : {}), }, }); } catch (error) { @@ -1092,6 +1133,18 @@ export class LauncherService { return this.r2Client; } + private async readPackageVersion(workDir: string) { + try { + const packageJson = await readFile(`${workDir}/package.json`, 'utf8'); + const parsed = JSON.parse(packageJson) as { version?: unknown }; + return typeof parsed.version === 'string' && parsed.version.trim() + ? parsed.version.trim() + : null; + } catch { + return null; + } + } + private createLauncherArtifactObjectKey(version: string, branch: string, commitSha: string | null, artifact: string) { const prefix = this.configService.get('R2_ARTIFACT_PREFIX', 'studio-artifacts'); const revision = commitSha?.slice(0, 7) || 'latest';