Compare commits

...

1 Commits

Author SHA1 Message Date
chyoo 9f67ee5719 fix(admin): filter non-container releases from deployments
ci/woodpecker/push/woodpecker Pipeline was successful
2026-08-13 17:53:48 +09:00
+50 -23
View File
@@ -929,36 +929,52 @@ export class AppsService {
? parseReleaseImageTags(release.imageTags).images ? parseReleaseImageTags(release.imageTags).images
: { [quickBuild!.component.name]: quickBuild!.image! }; : { [quickBuild!.component.name]: quickBuild!.image! };
const images = Object.fromEntries( const images = Object.fromEntries(
Object.entries(releaseImages).filter(([componentName]) => ( Object.entries(releaseImages).filter(([componentName]) => {
app.components.find((component) => component.name === componentName)?.type !== 'DATABASE' 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 const deploymentLabel = release
? `${app.name} ${release.version}` ? `${app.name} ${release.version}`
: `${app.name} ${quickBuild!.component.name} ${quickBuild!.commitSha?.slice(0, 7) ?? 'quick'}`; : `${app.name} ${quickBuild!.component.name} ${quickBuild!.commitSha?.slice(0, 7) ?? 'quick'}`;
const requestedResources = body.resources; const requestedResources = body.resources;
const runtimeProfile = await this.prisma.runtimeEnvProfile.findFirst({ const runtimeProfile = hasKubernetesWorkload
where: { ? await this.prisma.runtimeEnvProfile.findFirst({
appId: app.id, where: {
environment, appId: app.id,
namespace, environment,
}, namespace,
include: { },
ingressRules: { include: {
include: { component: true }, ingressRules: {
orderBy: { host: 'asc' }, include: { component: true },
}, orderBy: { host: 'asc' },
}, },
}); },
if (Object.keys(images).length === 0) { })
throw new BadRequestException('릴리즈에 배포 가능한 이미지가 없습니다.'); : 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 (hasKubernetesWorkload) {
if (!namespaceExists) { const namespaceExists = await this.namespaceExists(namespace);
throw new BadRequestException( if (!namespaceExists) {
`${namespace} namespace가 없습니다. 런타임 구성에서 Namespace 생성/확인을 먼저 실행하세요.`, throw new BadRequestException(
); `${namespace} namespace가 없습니다. 런타임 구성에서 Namespace 생성/확인을 먼저 실행하세요.`,
);
}
} }
const deploymentJob = await this.prisma.deploymentJob.create({ const deploymentJob = await this.prisma.deploymentJob.create({
@@ -975,6 +991,7 @@ export class AppsService {
`[running] deployment started: ${deploymentLabel}`, `[running] deployment started: ${deploymentLabel}`,
...(quickBuild ? [`[source] quick build ${quickBuild.id} ${quickBuild.image}`] : []), ...(quickBuild ? [`[source] quick build ${quickBuild.id} ${quickBuild.image}`] : []),
`[target] environment=${environment} namespace=${namespace}`, `[target] environment=${environment} namespace=${namespace}`,
...(isLauncherArtifactRelease ? ['[launcher] AppImage release; Kubernetes rollout skipped'] : []),
].join('\n'), ].join('\n'),
}, },
include: { include: {
@@ -3385,6 +3402,16 @@ function parseReleaseImageTags(value: Prisma.JsonValue | null) {
return { images }; 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<string, unknown>).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 }>) { function getUsedReleaseBuildIds(releases: Array<{ imageTags: Prisma.JsonValue | null }>) {
const ids = new Set<string>(); const ids = new Set<string>();
for (const release of releases) { for (const release of releases) {