read launcher build version from package json

This commit is contained in:
2026-07-03 10:45:55 +09:00
parent fa478b45b3
commit dd11e5186a
2 changed files with 71 additions and 16 deletions
+4 -2
View File
@@ -88,11 +88,13 @@ export class CreateLauncherBuildJobDto {
@IsString() @IsString()
commitSha?: string; commitSha?: string;
@IsOptional()
@IsString() @IsString()
version!: string; version?: string;
@IsOptional()
@IsString() @IsString()
artifact!: string; artifact?: string;
@IsOptional() @IsOptional()
@IsString() @IsString()
+67 -14
View File
@@ -3,7 +3,8 @@ import { ConfigService } from '@nestjs/config';
import { exec } from 'node:child_process'; import { exec } from 'node:child_process';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import { createReadStream } from 'node:fs'; 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 { promisify } from 'node:util';
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
@@ -764,11 +765,12 @@ export class LauncherService {
} }
async createBuildJob(body: CreateLauncherBuildJobDto) { async createBuildJob(body: CreateLauncherBuildJobDto) {
if (!body.version?.trim()) throw new BadRequestException('버전을 입력하세요.');
if (!body.artifact?.trim()) throw new BadRequestException('산출물 파일명을 입력하세요.');
const source = await this.getSourceSettings(); const source = await this.getSourceSettings();
const target = body.target?.trim() || 'launcher-client'; const target = body.target?.trim() || 'launcher-client';
const isBackend = target === 'launcher-backend'; 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 const repository = body.repository
? normalizeRepository(body.repository) ? normalizeRepository(body.repository)
: isBackend : isBackend
@@ -777,20 +779,21 @@ export class LauncherService {
const branch = body.branch?.trim() || (isBackend ? source.backend.branch : source.branch) || 'release'; 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 commitSha = body.commitSha?.trim() || (isBackend ? source.backend.commitSha : source.commitSha) || null;
const plan = isBackend const plan = isBackend
? this.createLauncherBackendImagePlan(body.version.trim(), body.artifact.trim(), body.uploadPath?.trim(), body.downloadUrl?.trim()) ? this.createLauncherBackendImagePlan(requestedVersion, requestedArtifact, body.uploadPath?.trim(), body.downloadUrl?.trim())
: this.createLauncherArtifactPlan(body.version.trim(), branch, commitSha, body.artifact.trim(), body.uploadPath?.trim(), body.downloadUrl?.trim()); : this.createLauncherArtifactPlan(requestedVersion, branch, commitSha, requestedArtifact, body.uploadPath?.trim(), body.downloadUrl?.trim());
if (!repository) throw new BadRequestException('소스 저장소가 연결되지 않았습니다.'); if (!repository) throw new BadRequestException('소스 저장소가 연결되지 않았습니다.');
if (!plan.uploadPath) throw new BadRequestException(isBackend ? 'Dockerfile 경로를 입력하세요.' : '업로드 경로를 입력하세요.'); if (!plan.uploadPath) throw new BadRequestException(isBackend ? 'Dockerfile 경로를 입력하세요.' : '업로드 경로를 입력하세요.');
if (!plan.downloadUrl) throw new BadRequestException(isBackend ? '이미지 태그를 입력하세요.' : '다운로드 URL을 입력하세요.'); if (!plan.downloadUrl) throw new BadRequestException(isBackend ? '이미지 태그를 입력하세요.' : '다운로드 URL을 입력하세요.');
const logs = [ const logs = [
`[queued] launcher build created: ${body.artifact.trim()}`, `[queued] launcher build created: ${requestedArtifact}`,
`[target] ${target}`, `[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',
isBackend ? `[plan] build and push image ${plan.downloadUrl}` : `[plan] build deb package`, 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}`, 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');
@@ -801,8 +804,8 @@ export class LauncherService {
repository, repository,
branch, branch,
commitSha, commitSha,
version: body.version.trim(), version: requestedVersion,
artifact: body.artifact.trim(), artifact: requestedArtifact,
uploadPath: plan.uploadPath, uploadPath: plan.uploadPath,
downloadUrl: plan.downloadUrl, downloadUrl: plan.downloadUrl,
logs, logs,
@@ -871,10 +874,17 @@ export class LauncherService {
} }
if (job.target === 'launcher-backend') { if (job.target === 'launcher-backend') {
const packageVersion = (await this.readPackageVersion(workDir)) || job.version;
const dockerfile = job.uploadPath || 'Dockerfile'; 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] dockerfile ${dockerfile}`);
await append(`[backend] image ${imageTag}`); await append(`[backend] image ${imageTag}`);
await this.prisma.launcherBuildJob.update({
where: { id: jobId },
data: { version: packageVersion, uploadPath: dockerfile, downloadUrl: imageTag },
});
await runCommand( await runCommand(
`docker build -t ${quote(imageTag)} -f ${quote(dockerfile)} .`, `docker build -t ${quote(imageTag)} -f ${quote(dockerfile)} .`,
workDir, workDir,
@@ -888,6 +898,9 @@ export class LauncherService {
data: { data: {
status: 'SUCCEEDED', status: 'SUCCEEDED',
finishedAt: new Date(), finishedAt: new Date(),
version: packageVersion,
uploadPath: dockerfile,
downloadUrl: imageTag,
logs, logs,
}, },
}); });
@@ -901,10 +914,35 @@ export class LauncherService {
const debPath = stdout.trim().split('\n').filter(Boolean)[0]; const debPath = stdout.trim().split('\n').filter(Boolean)[0];
if (!debPath) throw new Error('.deb 산출물을 찾지 못했습니다.'); 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) { if (!uploadedUrl) {
await mkdir(job.uploadPath, { recursive: true }); await mkdir(artifactPlan.uploadPath, { recursive: true });
const destination = `${job.uploadPath.replace(/\/$/, '')}/${job.artifact}`; const destination = `${artifactPlan.uploadPath.replace(/\/$/, '')}/${artifactName}`;
await copyFile(debPath, destination); await copyFile(debPath, destination);
await append(`[upload] ${destination}`); await append(`[upload] ${destination}`);
} }
@@ -915,8 +953,11 @@ export class LauncherService {
data: { data: {
status: 'SUCCEEDED', status: 'SUCCEEDED',
finishedAt: new Date(), finishedAt: new Date(),
version: packageVersion,
artifact: artifactName,
uploadPath: artifactPlan.uploadPath,
downloadUrl: uploadedUrl || artifactPlan.downloadUrl,
logs, logs,
...(uploadedUrl ? { downloadUrl: uploadedUrl } : {}),
}, },
}); });
} catch (error) { } catch (error) {
@@ -1092,6 +1133,18 @@ export class LauncherService {
return this.r2Client; 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) { private createLauncherArtifactObjectKey(version: string, branch: string, commitSha: string | null, artifact: string) {
const prefix = this.configService.get<string>('R2_ARTIFACT_PREFIX', 'studio-artifacts'); const prefix = this.configService.get<string>('R2_ARTIFACT_PREFIX', 'studio-artifacts');
const revision = commitSha?.slice(0, 7) || 'latest'; const revision = commitSha?.slice(0, 7) || 'latest';