diff --git a/src/launcher/dto.ts b/src/launcher/dto.ts index cd0c2bd..f9fe982 100644 --- a/src/launcher/dto.ts +++ b/src/launcher/dto.ts @@ -237,6 +237,26 @@ export class UpdateLauncherUserAppPermissionsDto { } + +export class LauncherBuildEnvVariableDto { + @IsString() + key!: string; + + @IsString() + value!: string; + + @IsOptional() + @IsBoolean() + isSecret?: boolean; +} + +export class UpdateLauncherBuildEnvDto { + @IsArray() + @ValidateNested({ each: true }) + @Type(() => LauncherBuildEnvVariableDto) + variables!: LauncherBuildEnvVariableDto[]; +} + export class CreateLauncherCommonSettingDto { @IsString() key!: string; diff --git a/src/launcher/launcher.controller.ts b/src/launcher/launcher.controller.ts index c8b591a..c0fa3a2 100644 --- a/src/launcher/launcher.controller.ts +++ b/src/launcher/launcher.controller.ts @@ -1,6 +1,6 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common'; -import { CreateLauncherAppLinkDto, CreateLauncherBuildJobDto, CreateLauncherCommonSettingDto, CreateLauncherVersionDto, UpdateLauncherSourceDto, LinkLauncherVersionSourceDto, MoveLauncherAppLinkDto, UpdateLauncherAppLinkDto, UpdateLauncherCommonSettingDto, UpdateLauncherUserAppPermissionsDto } from './dto'; +import { CreateLauncherAppLinkDto, CreateLauncherBuildJobDto, CreateLauncherCommonSettingDto, CreateLauncherVersionDto, UpdateLauncherSourceDto, LinkLauncherVersionSourceDto, MoveLauncherAppLinkDto, UpdateLauncherAppLinkDto, UpdateLauncherBuildEnvDto, UpdateLauncherCommonSettingDto, UpdateLauncherUserAppPermissionsDto } from './dto'; import { LauncherService } from './launcher.service'; @Controller('launcher') @@ -27,6 +27,17 @@ export class LauncherController { return this.launcherService.updateSource(body); } + + @Get('build-env') + listBuildEnv() { + return this.launcherService.listBuildEnv(); + } + + @Put('build-env') + updateBuildEnv(@Body() body: UpdateLauncherBuildEnvDto) { + return this.launcherService.updateBuildEnv(body); + } + @Get('builds') listBuildJobs() { return this.launcherService.listBuildJobs(); diff --git a/src/launcher/launcher.service.ts b/src/launcher/launcher.service.ts index 9d1128b..d56719e 100644 --- a/src/launcher/launcher.service.ts +++ b/src/launcher/launcher.service.ts @@ -18,11 +18,28 @@ import { UpdateLauncherSourceDto, LinkLauncherVersionSourceDto, UpdateLauncherAppLinkDto, + UpdateLauncherBuildEnvDto, UpdateLauncherCommonSettingDto, UpdateLauncherUserAppPermissionsDto, } from './dto'; const execAsync = promisify(exec); +const LAUNCHER_BUILD_ENV_PREFIX = 'launcher.client.build.env.'; +const DEFAULT_LAUNCHER_CLIENT_BUILD_ENV = [ + { + key: 'VITE_ATNMY_STDO_API_BASE_URL', + name: '런처 API 주소', + value: 'https://portal-api.autonomystudio.net', + description: '런처 클라이언트가 호출할 portal-api 주소', + }, + { + key: 'VITE_ATNMY_AUTH_API_BASE_URL', + name: '공통 인증 API 주소', + value: 'https://auth.autonomystudio.net', + description: '회원가입/비밀번호 재설정에서 호출할 공통 인증 서버 주소', + }, +] as const; + const LAUNCHER_SOURCE_SETTING_KEYS = { repository: 'launcher.client.source.repository', branch: 'launcher.client.source.branch', @@ -234,12 +251,42 @@ export class LauncherService { }; } + + private async getLauncherClientBuildEnv() { + const rows = await this.launcherPrisma.launcherCommonSetting.findMany({ + where: { + key: { + startsWith: LAUNCHER_BUILD_ENV_PREFIX, + }, + isActive: true, + }, + orderBy: { key: 'asc' }, + }); + + if (!rows.length) { + return DEFAULT_LAUNCHER_CLIENT_BUILD_ENV.map((variable) => ({ + key: variable.key, + value: this.configService.get(variable.key, variable.value), + isSecret: false, + })); + } + + return rows + .map((row) => ({ + key: row.key.slice(LAUNCHER_BUILD_ENV_PREFIX.length), + value: row.value, + isSecret: row.isSecret, + })) + .filter((row) => row.key); + } + private upsertCommonSettingByKey( key: string, name: string, type: string, value: string, description: string, + isSecret = false, ) { return this.launcherPrisma.launcherCommonSetting.upsert({ where: { key }, @@ -250,7 +297,7 @@ export class LauncherService { value, description, isActive: true, - isSecret: false, + isSecret, }, update: { name, @@ -258,11 +305,45 @@ export class LauncherService { value, description, isActive: true, - isSecret: false, + isSecret, }, }); } + + async listBuildEnv() { + const variables = await this.getLauncherClientBuildEnv(); + return { variables }; + } + + async updateBuildEnv(body: UpdateLauncherBuildEnvDto) { + const variables = normalizeBuildEnvVariables(body.variables ?? []); + const nextKeys = new Set(variables.map((variable) => variable.key)); + const existing = await this.launcherPrisma.launcherCommonSetting.findMany({ + where: { + key: { + startsWith: LAUNCHER_BUILD_ENV_PREFIX, + }, + }, + }); + + await this.launcherPrisma.$transaction([ + ...existing + .filter((setting) => !nextKeys.has(setting.key.slice(LAUNCHER_BUILD_ENV_PREFIX.length))) + .map((setting) => this.launcherPrisma.launcherCommonSetting.delete({ where: { id: setting.id } })), + ...variables.map((variable) => this.upsertCommonSettingByKey( + `${LAUNCHER_BUILD_ENV_PREFIX}${variable.key}`, + variable.key, + 'launcher-build-env', + variable.value, + `런처 클라이언트 빌드 환경변수: ${variable.key}`, + variable.isSecret, + )), + ]); + + return this.listBuildEnv(); + } + listBuildJobs() { return this.prisma.launcherBuildJob.findMany({ orderBy: { createdAt: 'desc' }, @@ -908,7 +989,11 @@ export class LauncherService { } const buildCommand = process.env.LAUNCHER_BUILD_COMMAND || 'npm ci && npm run build:linux'; - await runCommand(buildCommand, workDir, append); + const buildEnvVariables = await this.getLauncherClientBuildEnv(); + if (buildEnvVariables.length) { + await append(`[build-env] ${buildEnvVariables.map((variable) => `${variable.key}=${variable.isSecret ? '********' : variable.value}`).join(' ')}`); + } + await runCommand(buildCommand, workDir, append, buildCommand, mergeBuildEnv(process.env, buildEnvVariables)); const { stdout } = await runCommand(`find ${quote(workDir)} -type f -name '*.deb' | head -n 1`, workDir, append); const debPath = stdout.trim().split('\n').filter(Boolean)[0]; @@ -1228,11 +1313,12 @@ async function runCommand( cwd: string, append: (...lines: string[]) => Promise, safeCommand = command, + env: NodeJS.ProcessEnv = process.env, ) { await append(`[run] ${safeCommand}`); const result = await execAsync(command, { cwd, - env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, + env: { ...env, GIT_TERMINAL_PROMPT: '0' }, maxBuffer: 1024 * 1024 * 20, shell: '/bin/bash', }); @@ -1243,6 +1329,38 @@ async function runCommand( return result; } + +function normalizeBuildEnvVariables( + variables: Array<{ key?: string; value?: string; isSecret?: boolean }>, +) { + const byKey = new Map(); + for (const variable of variables) { + const key = variable.key?.trim() ?? ''; + if (!key) continue; + if (!/^VITE_[A-Za-z0-9_]+$/.test(key)) { + throw new BadRequestException(`런처 클라이언트 빌드 환경변수는 VITE_로 시작해야 합니다: ${key}`); + } + byKey.set(key, { + key, + value: variable.value ?? '', + isSecret: variable.isSecret ?? false, + }); + } + + return [...byKey.values()].sort((left, right) => left.key.localeCompare(right.key)); +} + +function mergeBuildEnv( + env: NodeJS.ProcessEnv, + variables: Array<{ key: string; value: string }>, +) { + const nextEnv: NodeJS.ProcessEnv = { ...env }; + for (const variable of variables) { + nextEnv[variable.key] = variable.value; + } + return nextEnv; +} + function trimOptional(value?: string) { return value === undefined ? null : value.trim(); }