inject launcher client build env from settings
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-06 10:39:34 +09:00
parent a06d200eac
commit bb0fa5c619
3 changed files with 154 additions and 5 deletions
+20
View File
@@ -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 { export class CreateLauncherCommonSettingDto {
@IsString() @IsString()
key!: string; key!: string;
+12 -1
View File
@@ -1,6 +1,6 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common'; 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'; import { LauncherService } from './launcher.service';
@Controller('launcher') @Controller('launcher')
@@ -27,6 +27,17 @@ export class LauncherController {
return this.launcherService.updateSource(body); 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') @Get('builds')
listBuildJobs() { listBuildJobs() {
return this.launcherService.listBuildJobs(); return this.launcherService.listBuildJobs();
+122 -4
View File
@@ -18,11 +18,28 @@ import {
UpdateLauncherSourceDto, UpdateLauncherSourceDto,
LinkLauncherVersionSourceDto, LinkLauncherVersionSourceDto,
UpdateLauncherAppLinkDto, UpdateLauncherAppLinkDto,
UpdateLauncherBuildEnvDto,
UpdateLauncherCommonSettingDto, UpdateLauncherCommonSettingDto,
UpdateLauncherUserAppPermissionsDto, UpdateLauncherUserAppPermissionsDto,
} from './dto'; } from './dto';
const execAsync = promisify(exec); 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 = { const LAUNCHER_SOURCE_SETTING_KEYS = {
repository: 'launcher.client.source.repository', repository: 'launcher.client.source.repository',
branch: 'launcher.client.source.branch', 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<string>(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( private upsertCommonSettingByKey(
key: string, key: string,
name: string, name: string,
type: string, type: string,
value: string, value: string,
description: string, description: string,
isSecret = false,
) { ) {
return this.launcherPrisma.launcherCommonSetting.upsert({ return this.launcherPrisma.launcherCommonSetting.upsert({
where: { key }, where: { key },
@@ -250,7 +297,7 @@ export class LauncherService {
value, value,
description, description,
isActive: true, isActive: true,
isSecret: false, isSecret,
}, },
update: { update: {
name, name,
@@ -258,11 +305,45 @@ export class LauncherService {
value, value,
description, description,
isActive: true, 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() { listBuildJobs() {
return this.prisma.launcherBuildJob.findMany({ return this.prisma.launcherBuildJob.findMany({
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
@@ -908,7 +989,11 @@ export class LauncherService {
} }
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); 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 { stdout } = await runCommand(`find ${quote(workDir)} -type f -name '*.deb' | head -n 1`, workDir, append);
const debPath = stdout.trim().split('\n').filter(Boolean)[0]; const debPath = stdout.trim().split('\n').filter(Boolean)[0];
@@ -1228,11 +1313,12 @@ async function runCommand(
cwd: string, cwd: string,
append: (...lines: string[]) => Promise<void>, append: (...lines: string[]) => Promise<void>,
safeCommand = command, safeCommand = command,
env: NodeJS.ProcessEnv = process.env,
) { ) {
await append(`[run] ${safeCommand}`); await append(`[run] ${safeCommand}`);
const result = await execAsync(command, { const result = await execAsync(command, {
cwd, cwd,
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, env: { ...env, GIT_TERMINAL_PROMPT: '0' },
maxBuffer: 1024 * 1024 * 20, maxBuffer: 1024 * 1024 * 20,
shell: '/bin/bash', shell: '/bin/bash',
}); });
@@ -1243,6 +1329,38 @@ async function runCommand(
return result; return result;
} }
function normalizeBuildEnvVariables(
variables: Array<{ key?: string; value?: string; isSecret?: boolean }>,
) {
const byKey = new Map<string, { key: string; value: string; isSecret: boolean }>();
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) { function trimOptional(value?: string) {
return value === undefined ? null : value.trim(); return value === undefined ? null : value.trim();
} }