paginate app operations and refine runtime settings layout
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-07 17:08:49 +09:00
parent 5be7f0980b
commit cc5a15a01c
3 changed files with 118 additions and 18 deletions
+2
View File
@@ -9,6 +9,8 @@ AUTH_SERVER_URL=http://localhost:4100
GITEA_BASE_URL=http://localhost:3000 GITEA_BASE_URL=http://localhost:3000
GITEA_APP_ORG=platform GITEA_APP_ORG=platform
GITEA_ADMIN_TOKEN= GITEA_ADMIN_TOKEN=
GITEA_RELEASE_PUSH_WHITELIST=
GITEA_RELEASE_MERGE_WHITELIST=
BUILD_REGISTRY=localhost:5000 BUILD_REGISTRY=localhost:5000
BUILD_ARTIFACT_INSTALL_COMMAND=npm ci BUILD_ARTIFACT_INSTALL_COMMAND=npm ci
LAUNCHER_BUILD_COMMAND="npm ci && npm run build:linux" LAUNCHER_BUILD_COMMAND="npm ci && npm run build:linux"
+40 -18
View File
@@ -1143,26 +1143,52 @@ export class AppsService {
}, },
}); });
const logs = splitDeploymentLogs(migrationJob.logs); void this.finalizeMigrationJob(migrationJob.id, {
try {
logs.push(`[apply] job/${jobName}`);
appendOutput(
logs,
await this.applyK8sManifest(createMigrationJobManifest({
namespace, namespace,
jobName, jobName,
image, image,
command, command,
envSecretName, 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/${input.jobName}`);
appendOutput(
logs,
await this.applyK8sManifest(createMigrationJobManifest({
namespace: input.namespace,
jobName: input.jobName,
image: input.image,
command: input.command,
envSecretName: input.envSecretName,
})), })),
); );
logs.push(`[wait] job/${jobName}`); logs.push(`[wait] job/${input.jobName}`);
await this.waitForJobResult(namespace, jobName, logs); await this.waitForJobResult(input.namespace, input.jobName, logs);
logs.push(`[logs] job/${jobName}`); logs.push(`[logs] job/${input.jobName}`);
appendOutput(logs, await this.kubectl(['logs', `job/${jobName}`, '-n', namespace])); appendOutput(logs, await this.kubectl(['logs', `job/${input.jobName}`, '-n', input.namespace]));
logs.push('[done] migration completed'); logs.push('[done] migration completed');
const completed = await this.prisma.runtimeControlJob.update({ await this.prisma.runtimeControlJob.update({
where: { id: migrationJob.id }, where: { id: migrationJob.id },
data: { data: {
status: 'SUCCEEDED', status: 'SUCCEEDED',
@@ -1170,19 +1196,17 @@ export class AppsService {
logs: logs.join('\n'), logs: logs.join('\n'),
}, },
}); });
return { migrationJob: completed };
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : 'Migration failed'; const message = error instanceof Error ? error.message : 'Migration failed';
try { try {
logs.push(`[logs] job/${jobName}`); logs.push(`[logs] job/${input.jobName}`);
appendOutput(logs, await this.kubectl(['logs', `job/${jobName}`, '-n', namespace])); appendOutput(logs, await this.kubectl(['logs', `job/${input.jobName}`, '-n', input.namespace]));
} catch { } catch {
// 실패한 Job 로그가 아직 없을 수 있습니다. // 실패한 Job 로그가 아직 없을 수 있습니다.
} }
logs.push(`[failed] ${message}`); logs.push(`[failed] ${message}`);
const failed = await this.prisma.runtimeControlJob.update({ await this.prisma.runtimeControlJob.update({
where: { id: migrationJob.id }, where: { id: migrationJob.id },
data: { data: {
status: 'FAILED', status: 'FAILED',
@@ -1190,8 +1214,6 @@ export class AppsService {
logs: logs.join('\n'), logs: logs.join('\n'),
}, },
}); });
return { migrationJob: failed };
} }
} }
+76
View File
@@ -34,6 +34,8 @@ export class GiteaService {
private readonly baseUrl: string; private readonly baseUrl: string;
private readonly appOrg: string; private readonly appOrg: string;
private readonly token?: string; private readonly token?: string;
private readonly releaseBranchPushUsers: string[];
private readonly releaseBranchMergeUsers: string[];
constructor(configService: ConfigService) { constructor(configService: ConfigService) {
this.baseUrl = configService this.baseUrl = configService
@@ -41,6 +43,12 @@ export class GiteaService {
.replace(/\/$/, ''); .replace(/\/$/, '');
this.appOrg = configService.get<string>('GITEA_APP_ORG', 'platform'); this.appOrg = configService.get<string>('GITEA_APP_ORG', 'platform');
this.token = configService.get<string>('GITEA_ADMIN_TOKEN') || undefined; this.token = configService.get<string>('GITEA_ADMIN_TOKEN') || undefined;
this.releaseBranchPushUsers = parseWhiteList(
configService.get<string>('GITEA_RELEASE_PUSH_WHITELIST'),
);
this.releaseBranchMergeUsers = parseWhiteList(
configService.get<string>('GITEA_RELEASE_MERGE_WHITELIST'),
);
} }
async getVersion() { async getVersion() {
@@ -159,6 +167,10 @@ export class GiteaService {
await this.createDefaultBranches(repo.owner.login, repo.name, repo.default_branch, body); 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) }; 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); 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) }; return { repo: mapRepo(repo), branches: await this.listBranches(org, repo.name) };
} }
@@ -241,6 +257,42 @@ export class GiteaService {
return createdBranches; 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<T>( private async request<T>(
path: string, path: string,
init?: RequestInit & { allowedStatuses?: number[] }, 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<string>();
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) { function toCreateRepoPayload(body: CreateGiteaRepoDto) {
return { return {
name: body.name, name: body.name,