diff --git a/src/apps/apps.service.ts b/src/apps/apps.service.ts index 7a915a2..7500aed 100644 --- a/src/apps/apps.service.ts +++ b/src/apps/apps.service.ts @@ -929,36 +929,52 @@ export class AppsService { ? parseReleaseImageTags(release.imageTags).images : { [quickBuild!.component.name]: quickBuild!.image! }; const images = Object.fromEntries( - Object.entries(releaseImages).filter(([componentName]) => ( - app.components.find((component) => component.name === componentName)?.type !== 'DATABASE' - )), + Object.entries(releaseImages).filter(([componentName]) => { + const componentType = app.components.find((component) => component.name === componentName)?.type; + return componentType === 'CONTAINER' || componentType === 'STATIC_WEB'; + }), + ); + const hasKubernetesWorkload = Object.keys(images).length > 0; + const isLauncherArtifactRelease = Boolean( + release + && !hasKubernetesWorkload + && body.updateLauncherAppVersion !== false + && app.components.some((component) => component.type === 'ELECTRON') + && hasReleaseArtifact(release.artifact, release.imageTags), ); const deploymentLabel = release ? `${app.name} ${release.version}` : `${app.name} ${quickBuild!.component.name} ${quickBuild!.commitSha?.slice(0, 7) ?? 'quick'}`; const requestedResources = body.resources; - const runtimeProfile = await this.prisma.runtimeEnvProfile.findFirst({ - where: { - appId: app.id, - environment, - namespace, - }, - include: { - ingressRules: { - include: { component: true }, - orderBy: { host: 'asc' }, - }, - }, - }); - if (Object.keys(images).length === 0) { - throw new BadRequestException('릴리즈에 배포 가능한 이미지가 없습니다.'); + const runtimeProfile = hasKubernetesWorkload + ? await this.prisma.runtimeEnvProfile.findFirst({ + where: { + appId: app.id, + environment, + namespace, + }, + include: { + ingressRules: { + include: { component: true }, + orderBy: { host: 'asc' }, + }, + }, + }) + : null; + if (!hasKubernetesWorkload && !isLauncherArtifactRelease) { + if (release && hasReleaseArtifact(release.artifact, release.imageTags)) { + throw new BadRequestException('AppImage 릴리즈는 런처 앱 버전 업데이트를 선택해야 합니다.'); + } + throw new BadRequestException('릴리즈에 배포 가능한 이미지 또는 AppImage 산출물이 없습니다.'); } - const namespaceExists = await this.namespaceExists(namespace); - if (!namespaceExists) { - throw new BadRequestException( - `${namespace} namespace가 없습니다. 런타임 구성에서 Namespace 생성/확인을 먼저 실행하세요.`, - ); + if (hasKubernetesWorkload) { + const namespaceExists = await this.namespaceExists(namespace); + if (!namespaceExists) { + throw new BadRequestException( + `${namespace} namespace가 없습니다. 런타임 구성에서 Namespace 생성/확인을 먼저 실행하세요.`, + ); + } } const deploymentJob = await this.prisma.deploymentJob.create({ @@ -975,6 +991,7 @@ export class AppsService { `[running] deployment started: ${deploymentLabel}`, ...(quickBuild ? [`[source] quick build ${quickBuild.id} ${quickBuild.image}`] : []), `[target] environment=${environment} namespace=${namespace}`, + ...(isLauncherArtifactRelease ? ['[launcher] AppImage release; Kubernetes rollout skipped'] : []), ].join('\n'), }, include: { @@ -3385,6 +3402,16 @@ function parseReleaseImageTags(value: Prisma.JsonValue | null) { return { images }; } +function hasReleaseArtifact(artifact: string | null, imageTags: Prisma.JsonValue | null) { + if (artifact?.trim()) return true; + if (!imageTags || typeof imageTags !== 'object' || Array.isArray(imageTags)) return false; + + const artifacts = (imageTags as Record).artifacts; + if (!artifacts || typeof artifacts !== 'object' || Array.isArray(artifacts)) return false; + + return Object.values(artifacts).some((value) => typeof value === 'string' && value.trim()); +} + function getUsedReleaseBuildIds(releases: Array<{ imageTags: Prisma.JsonValue | null }>) { const ids = new Set(); for (const release of releases) {