This commit is contained in:
@@ -210,13 +210,13 @@ export function BuildManagementPage() {
|
|||||||
<PageTitle
|
<PageTitle
|
||||||
eyebrow="어플리케이션"
|
eyebrow="어플리케이션"
|
||||||
title="빌드 관리"
|
title="빌드 관리"
|
||||||
description="저장소 소스에서 컨테이너 이미지와 런처 다운로드용 클라이언트 산출물을 만드는 작업을 관리합니다."
|
description="develop 브랜치의 빌드 검증 결과를 만들고, 성공한 작업을 릴리즈 후보로 넘깁니다."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppSelector apps={apps} value={selectedAppId} loading={appsLoading} onChange={selectApp} />
|
<AppSelector apps={apps} value={selectedAppId} loading={appsLoading} onChange={selectApp} />
|
||||||
|
|
||||||
<Card className="mb-4">
|
<Card className="mb-4">
|
||||||
<CardHeader title="빌드 요청" description="구성요소, 브랜치, 커밋 기준으로 빌드 작업을 생성합니다." />
|
<CardHeader title="빌드 요청" description="기본적으로 develop 최신 커밋을 검증합니다. 특정 커밋을 고정하려면 Commit SHA를 입력합니다." />
|
||||||
<div className="grid gap-4 p-5 xl:grid-cols-[1.2fr_1fr]">
|
<div className="grid gap-4 p-5 xl:grid-cols-[1.2fr_1fr]">
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ function buildValue(build: BuildJob) {
|
|||||||
return build.image || build.artifact || '';
|
return build.image || build.artifact || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildCommit(build: BuildJob) {
|
||||||
|
return build.commitSha?.trim() || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDevelopBuild(build: BuildJob) {
|
||||||
|
return build.branch === 'develop';
|
||||||
|
}
|
||||||
|
|
||||||
function buildTime(build: BuildJob) {
|
function buildTime(build: BuildJob) {
|
||||||
const value = new Date(build.finishedAt || build.createdAt).getTime();
|
const value = new Date(build.finishedAt || build.createdAt).getTime();
|
||||||
return Number.isNaN(value) ? 0 : value;
|
return Number.isNaN(value) ? 0 : value;
|
||||||
@@ -72,6 +80,17 @@ function latestBuildIdsByComponent(builds: BuildJob[]) {
|
|||||||
return [...map.values()].map((build) => build.id);
|
return [...map.values()].map((build) => build.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function latestCandidateBuilds(builds: BuildJob[]) {
|
||||||
|
const latest = new Map<string, BuildJob>();
|
||||||
|
for (const build of builds) {
|
||||||
|
const current = latest.get(build.componentId);
|
||||||
|
if (!current || buildTime(build) > buildTime(current)) {
|
||||||
|
latest.set(build.componentId, build);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...latest.values()].sort((a, b) => buildTime(b) - buildTime(a));
|
||||||
|
}
|
||||||
|
|
||||||
function newestBuildVersion(builds: BuildJob[]) {
|
function newestBuildVersion(builds: BuildJob[]) {
|
||||||
return builds
|
return builds
|
||||||
.slice()
|
.slice()
|
||||||
@@ -87,17 +106,18 @@ function buildCount(release: AppRelease) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function groupCandidateBuilds(builds: BuildJob[]) {
|
function groupCandidateBuilds(builds: BuildJob[]) {
|
||||||
const map = new Map<string, { appId: string; appName: string; count: number }>();
|
const map = new Map<string, { appId: string; appName: string; count: number; latestAt: number }>();
|
||||||
for (const build of builds) {
|
for (const build of builds) {
|
||||||
const appId = build.component.app.id;
|
const appId = build.component.app.id;
|
||||||
const current = map.get(appId);
|
const current = map.get(appId);
|
||||||
map.set(appId, {
|
map.set(appId, {
|
||||||
appId,
|
appId,
|
||||||
appName: build.component.app.name,
|
appName: build.component.app.name,
|
||||||
count: (current?.count ?? 0) + 1
|
count: (current?.count ?? 0) + 1,
|
||||||
|
latestAt: Math.max(current?.latestAt ?? 0, buildTime(build))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return [...map.values()];
|
return [...map.values()].sort((a, b) => b.latestAt - a.latestAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
function releaseTime(release: AppRelease) {
|
function releaseTime(release: AppRelease) {
|
||||||
@@ -114,7 +134,6 @@ export function ReleaseManagementPage() {
|
|||||||
const [builds, setBuilds] = useState<BuildJob[]>([]);
|
const [builds, setBuilds] = useState<BuildJob[]>([]);
|
||||||
const [version, setVersion] = useState('');
|
const [version, setVersion] = useState('');
|
||||||
const [releaseModalOpen, setReleaseModalOpen] = useState(false);
|
const [releaseModalOpen, setReleaseModalOpen] = useState(false);
|
||||||
const [branch, setBranch] = useState('release');
|
|
||||||
const [selectedBuildIds, setSelectedBuildIds] = useState<string[]>([]);
|
const [selectedBuildIds, setSelectedBuildIds] = useState<string[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
@@ -123,7 +142,7 @@ export function ReleaseManagementPage() {
|
|||||||
const candidateBuilds = useMemo(
|
const candidateBuilds = useMemo(
|
||||||
() => builds.filter((build) => {
|
() => builds.filter((build) => {
|
||||||
const value = buildValue(build);
|
const value = buildValue(build);
|
||||||
return !value || !usedValues.has(value);
|
return isDevelopBuild(build) && Boolean(buildCommit(build)) && (!value || !usedValues.has(value));
|
||||||
}),
|
}),
|
||||||
[builds, usedValues]
|
[builds, usedValues]
|
||||||
);
|
);
|
||||||
@@ -174,7 +193,8 @@ export function ReleaseManagementPage() {
|
|||||||
const nextAppId = appId || selectedAppId || candidateGroups[0]?.appId || '';
|
const nextAppId = appId || selectedAppId || candidateGroups[0]?.appId || '';
|
||||||
if (nextAppId && nextAppId !== selectedAppId) selectApp(nextAppId);
|
if (nextAppId && nextAppId !== selectedAppId) selectApp(nextAppId);
|
||||||
const appBuilds = candidateBuilds.filter((build) => build.component.app.id === nextAppId);
|
const appBuilds = candidateBuilds.filter((build) => build.component.app.id === nextAppId);
|
||||||
setSelectedBuildIds(latestBuildIdsByComponent(appBuilds));
|
const latestBuilds = latestCandidateBuilds(appBuilds);
|
||||||
|
setSelectedBuildIds(latestBuildIdsByComponent(latestBuilds));
|
||||||
setVersion((prev) => prev || newestBuildVersion(appBuilds));
|
setVersion((prev) => prev || newestBuildVersion(appBuilds));
|
||||||
setReleaseModalOpen(true);
|
setReleaseModalOpen(true);
|
||||||
};
|
};
|
||||||
@@ -186,7 +206,7 @@ export function ReleaseManagementPage() {
|
|||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (!selectedAppId) return;
|
if (!selectedAppId) return;
|
||||||
try {
|
try {
|
||||||
await createRelease({ appId: selectedAppId, version, branch, buildJobIds: selectedBuildIds });
|
await createRelease({ appId: selectedAppId, version, branch: 'release', buildJobIds: selectedBuildIds });
|
||||||
toast({ type: 'success', message: '릴리즈가 생성되었습니다.' });
|
toast({ type: 'success', message: '릴리즈가 생성되었습니다.' });
|
||||||
setVersion('');
|
setVersion('');
|
||||||
setSelectedBuildIds([]);
|
setSelectedBuildIds([]);
|
||||||
@@ -202,7 +222,7 @@ export function ReleaseManagementPage() {
|
|||||||
<PageTitle
|
<PageTitle
|
||||||
eyebrow="어플리케이션"
|
eyebrow="어플리케이션"
|
||||||
title="릴리즈 관리"
|
title="릴리즈 관리"
|
||||||
description="브랜치, 커밋, 빌드 산출물을 묶어 환경별로 배포 가능한 릴리즈를 생성합니다."
|
description="검증에 성공한 develop 빌드 결과를 release 브랜치로 승격해 배포 가능한 릴리즈를 생성합니다."
|
||||||
actions={
|
actions={
|
||||||
<Button className="h-8 px-3 text-[13px]" disabled={candidateBuilds.length === 0} onClick={() => openReleaseModal()}>
|
<Button className="h-8 px-3 text-[13px]" disabled={candidateBuilds.length === 0} onClick={() => openReleaseModal()}>
|
||||||
<Plus className="size-4" />
|
<Plus className="size-4" />
|
||||||
@@ -279,7 +299,7 @@ export function ReleaseManagementPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="min-w-0 overflow-hidden">
|
<Card className="min-w-0 overflow-hidden">
|
||||||
<CardHeader title="릴리즈 후보" description="성공한 빌드 작업 중 릴리즈에 포함 가능한 항목입니다." />
|
<CardHeader title="릴리즈 후보" description="아직 릴리즈에 포함되지 않은 develop 성공 빌드입니다." />
|
||||||
<div className="space-y-3 p-4">
|
<div className="space-y-3 p-4">
|
||||||
{candidateGroups.length === 0 ? (
|
{candidateGroups.length === 0 ? (
|
||||||
<div className="rounded-md border border-border px-3 py-4 text-center text-sm text-muted-foreground">
|
<div className="rounded-md border border-border px-3 py-4 text-center text-sm text-muted-foreground">
|
||||||
@@ -312,7 +332,9 @@ export function ReleaseManagementPage() {
|
|||||||
{candidateGroups.map((group) => <option key={group.appId} value={group.appId}>{group.appName}</option>)}
|
{candidateGroups.map((group) => <option key={group.appId} value={group.appId}>{group.appName}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<input className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none" value={version} onChange={(event) => setVersion(event.target.value)} placeholder="버전 예: 0.1.1" />
|
<input className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none" value={version} onChange={(event) => setVersion(event.target.value)} placeholder="버전 예: 0.1.1" />
|
||||||
<input className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none" value={branch} onChange={(event) => setBranch(event.target.value)} placeholder="브랜치" />
|
<div className="rounded-md border border-border bg-muted/20 px-3 py-2 text-xs leading-5 text-muted-foreground">
|
||||||
|
develop 성공 빌드만 선택할 수 있으며, 생성 시 각 저장소의 release 브랜치를 선택한 commit으로 fast-forward 합니다.
|
||||||
|
</div>
|
||||||
<div className="max-h-72 space-y-2 overflow-y-auto rounded-md border border-border p-2">
|
<div className="max-h-72 space-y-2 overflow-y-auto rounded-md border border-border p-2">
|
||||||
{modalBuilds.length === 0 ? (
|
{modalBuilds.length === 0 ? (
|
||||||
<div className="px-2 py-8 text-center text-sm text-muted-foreground">
|
<div className="px-2 py-8 text-center text-sm text-muted-foreground">
|
||||||
@@ -329,6 +351,7 @@ export function ReleaseManagementPage() {
|
|||||||
<span className="font-medium">{job.component.name}</span>
|
<span className="font-medium">{job.component.name}</span>
|
||||||
{artifactVersion ? <Badge tone="blue">v{artifactVersion}</Badge> : <Badge tone="neutral">버전 없음</Badge>}
|
{artifactVersion ? <Badge tone="blue">v{artifactVersion}</Badge> : <Badge tone="neutral">버전 없음</Badge>}
|
||||||
<span className="text-xs text-muted-foreground">{job.branch}</span>
|
<span className="text-xs text-muted-foreground">{job.branch}</span>
|
||||||
|
<span className="font-mono text-xs text-muted-foreground">{buildCommit(job).slice(0, 7)}</span>
|
||||||
<span className="text-xs text-muted-foreground">{formatDate(job.finishedAt || job.createdAt)}</span>
|
<span className="text-xs text-muted-foreground">{formatDate(job.finishedAt || job.createdAt)}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="mt-1 block truncate text-xs text-muted-foreground">{job.image || job.artifact || job.commitSha}</span>
|
<span className="mt-1 block truncate text-xs text-muted-foreground">{job.image || job.artifact || job.commitSha}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user