inject launcher client build env from settings
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user