From cc5a15a01cb6392ffa7be17c6ea5b32c2ee026c8 Mon Sep 17 00:00:00 2001 From: chyoo Date: Tue, 7 Jul 2026 17:08:49 +0900 Subject: [PATCH] paginate app operations and refine runtime settings layout --- .env.example | 2 + src/apps/apps.service.ts | 58 ++++++++++++++++++++--------- src/gitea/gitea.service.ts | 76 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 18 deletions(-) diff --git a/.env.example b/.env.example index 6bd7072..c9995f0 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,8 @@ AUTH_SERVER_URL=http://localhost:4100 GITEA_BASE_URL=http://localhost:3000 GITEA_APP_ORG=platform GITEA_ADMIN_TOKEN= +GITEA_RELEASE_PUSH_WHITELIST= +GITEA_RELEASE_MERGE_WHITELIST= BUILD_REGISTRY=localhost:5000 BUILD_ARTIFACT_INSTALL_COMMAND=npm ci LAUNCHER_BUILD_COMMAND="npm ci && npm run build:linux" diff --git a/src/apps/apps.service.ts b/src/apps/apps.service.ts index c3ee9a8..5912e1a 100644 --- a/src/apps/apps.service.ts +++ b/src/apps/apps.service.ts @@ -1143,26 +1143,52 @@ export class AppsService { }, }); + void this.finalizeMigrationJob(migrationJob.id, { + namespace, + jobName, + image, + command, + envSecretName, + }); + + return { migrationJob }; + } + + private async finalizeMigrationJob( + migrationJobId: string, + input: { + namespace: string; + jobName: string; + image: string; + command: string; + envSecretName: string; + }, + ) { + const migrationJob = await this.prisma.runtimeControlJob.findUnique({ + where: { id: migrationJobId }, + }); + if (!migrationJob) return; + const logs = splitDeploymentLogs(migrationJob.logs); try { - logs.push(`[apply] job/${jobName}`); + logs.push(`[apply] job/${input.jobName}`); appendOutput( logs, await this.applyK8sManifest(createMigrationJobManifest({ - namespace, - jobName, - image, - command, - envSecretName, + namespace: input.namespace, + jobName: input.jobName, + image: input.image, + command: input.command, + envSecretName: input.envSecretName, })), ); - logs.push(`[wait] job/${jobName}`); - await this.waitForJobResult(namespace, jobName, logs); - logs.push(`[logs] job/${jobName}`); - appendOutput(logs, await this.kubectl(['logs', `job/${jobName}`, '-n', namespace])); + logs.push(`[wait] job/${input.jobName}`); + await this.waitForJobResult(input.namespace, input.jobName, logs); + logs.push(`[logs] job/${input.jobName}`); + appendOutput(logs, await this.kubectl(['logs', `job/${input.jobName}`, '-n', input.namespace])); logs.push('[done] migration completed'); - const completed = await this.prisma.runtimeControlJob.update({ + await this.prisma.runtimeControlJob.update({ where: { id: migrationJob.id }, data: { status: 'SUCCEEDED', @@ -1170,19 +1196,17 @@ export class AppsService { logs: logs.join('\n'), }, }); - - return { migrationJob: completed }; } catch (error) { const message = error instanceof Error ? error.message : 'Migration failed'; try { - logs.push(`[logs] job/${jobName}`); - appendOutput(logs, await this.kubectl(['logs', `job/${jobName}`, '-n', namespace])); + logs.push(`[logs] job/${input.jobName}`); + appendOutput(logs, await this.kubectl(['logs', `job/${input.jobName}`, '-n', input.namespace])); } catch { // 실패한 Job 로그가 아직 없을 수 있습니다. } logs.push(`[failed] ${message}`); - const failed = await this.prisma.runtimeControlJob.update({ + await this.prisma.runtimeControlJob.update({ where: { id: migrationJob.id }, data: { status: 'FAILED', @@ -1190,8 +1214,6 @@ export class AppsService { logs: logs.join('\n'), }, }); - - return { migrationJob: failed }; } } diff --git a/src/gitea/gitea.service.ts b/src/gitea/gitea.service.ts index 97a3d76..d537a38 100644 --- a/src/gitea/gitea.service.ts +++ b/src/gitea/gitea.service.ts @@ -34,6 +34,8 @@ export class GiteaService { private readonly baseUrl: string; private readonly appOrg: string; private readonly token?: string; + private readonly releaseBranchPushUsers: string[]; + private readonly releaseBranchMergeUsers: string[]; constructor(configService: ConfigService) { this.baseUrl = configService @@ -41,6 +43,12 @@ export class GiteaService { .replace(/\/$/, ''); this.appOrg = configService.get('GITEA_APP_ORG', 'platform'); this.token = configService.get('GITEA_ADMIN_TOKEN') || undefined; + this.releaseBranchPushUsers = parseWhiteList( + configService.get('GITEA_RELEASE_PUSH_WHITELIST'), + ); + this.releaseBranchMergeUsers = parseWhiteList( + configService.get('GITEA_RELEASE_MERGE_WHITELIST'), + ); } async getVersion() { @@ -159,6 +167,10 @@ export class GiteaService { await this.createDefaultBranches(repo.owner.login, repo.name, repo.default_branch, body); } + if (body.createReleaseBranch !== false) { + await this.protectReleaseBranch(repo.owner.login, repo.name); + } + return { repo: mapRepo(repo), branches: await this.listBranches(repo.owner.login, repo.name) }; } @@ -174,6 +186,10 @@ export class GiteaService { await this.createDefaultBranches(org, repo.name, repo.default_branch, body); } + if (body.createReleaseBranch !== false) { + await this.protectReleaseBranch(org, repo.name); + } + return { repo: mapRepo(repo), branches: await this.listBranches(org, repo.name) }; } @@ -241,6 +257,42 @@ export class GiteaService { return createdBranches; } + private async protectReleaseBranch(owner: string, repo: string) { + const me = await this.getAuthenticatedUser(); + const pushUsernames = uniqueUsers([ + ...this.releaseBranchPushUsers, + ...(me?.login ? [me.login] : []), + ]); + const mergeUsernames = uniqueUsers([ + ...this.releaseBranchMergeUsers, + ...(me?.login ? [me.login] : []), + ]); + + const body = { + branch_name: 'release', + enable_push: true, + enable_push_whitelist: true, + push_whitelist_usernames: pushUsernames, + enable_merge_whitelist: true, + merge_whitelist_usernames: mergeUsernames, + enable_force_push: false, + }; + + await this.request(`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branch_protections`, { + method: 'POST', + body: JSON.stringify(body), + allowedStatuses: [409], + }); + } + + private async getAuthenticatedUser() { + try { + return await this.request<{ login: string }>('/api/v1/user'); + } catch { + return null; + } + } + private async request( path: string, init?: RequestInit & { allowedStatuses?: number[] }, @@ -373,6 +425,30 @@ function mapRepo(repo: GiteaRepo) { }; } +function parseWhiteList(raw?: string) { + if (!raw) { + return []; + } + + return raw + .split(',') + .map((value) => value.trim()) + .filter((value) => value.length > 0); +} + +function uniqueUsers(values: string[]) { + const seen = new Set(); + const next: string[] = []; + + for (const value of values) { + if (seen.has(value)) continue; + seen.add(value); + next.push(value); + } + + return next; +} + function toCreateRepoPayload(body: CreateGiteaRepoDto) { return { name: body.name,