feat: 增加项目 Gitea 诊断接口
- Gitea client: 增加仓库元数据读取 - projects: 新增 gitea-diagnostics 聚合接口 - project types: 补齐仓库诊断安全响应模型
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* 封装 Gitea HTTP API 调用,统一处理鉴权、仓库元数据读取和错误脱敏。
|
||||
*/
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { IntegrationRequestError, IntegrationUnavailableError } from '../../common/errors/integration-error';
|
||||
@@ -7,6 +10,7 @@ import {
|
||||
GiteaBranch,
|
||||
GiteaCommitSummary,
|
||||
GiteaPullRequestSummary,
|
||||
GiteaRepositoryInfo,
|
||||
GiteaTag,
|
||||
} from './gitea.types';
|
||||
|
||||
@@ -43,6 +47,37 @@ export class GiteaClient {
|
||||
}));
|
||||
}
|
||||
|
||||
async getRepository(owner: string, repo: string): Promise<GiteaRepositoryInfo> {
|
||||
const payload = await this.request<{
|
||||
name: string;
|
||||
full_name?: string;
|
||||
default_branch?: string;
|
||||
html_url?: string;
|
||||
clone_url?: string;
|
||||
archived?: boolean;
|
||||
private?: boolean;
|
||||
empty?: boolean;
|
||||
updated_at?: string;
|
||||
owner?: {
|
||||
login?: string;
|
||||
username?: string;
|
||||
};
|
||||
}>(`/api/v1/repos/${owner}/${repo}`);
|
||||
|
||||
return {
|
||||
owner: payload.owner?.login ?? payload.owner?.username ?? owner,
|
||||
name: payload.name,
|
||||
fullName: payload.full_name,
|
||||
defaultBranch: payload.default_branch,
|
||||
webUrl: payload.html_url,
|
||||
cloneUrl: payload.clone_url,
|
||||
archived: payload.archived,
|
||||
private: payload.private,
|
||||
empty: payload.empty,
|
||||
updatedAt: payload.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
async getCommit(
|
||||
owner: string,
|
||||
repo: string,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Gitea API 的前端安全摘要类型,只保存仓库和提交元数据,不包含 token。
|
||||
*/
|
||||
export type GiteaBranch = {
|
||||
name: string;
|
||||
commitSha: string;
|
||||
@@ -27,3 +30,16 @@ export type GiteaPullRequestSummary = {
|
||||
baseRef?: string;
|
||||
authorName?: string;
|
||||
};
|
||||
|
||||
export type GiteaRepositoryInfo = {
|
||||
owner: string;
|
||||
name: string;
|
||||
fullName?: string;
|
||||
defaultBranch?: string;
|
||||
webUrl?: string;
|
||||
cloneUrl?: string;
|
||||
archived?: boolean;
|
||||
private?: boolean;
|
||||
empty?: boolean;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* 项目模块对前端暴露的安全模型;第三方 token 只在后端使用,不进入响应。
|
||||
*/
|
||||
export type ProjectEnvironmentSummary = {
|
||||
name: 'test' | 'production';
|
||||
displayName: string;
|
||||
@@ -68,3 +71,32 @@ export type ProjectJenkinsDiagnosticsSummary = {
|
||||
checkedAt: string;
|
||||
jobs: ProjectJenkinsJobDiagnostic[];
|
||||
};
|
||||
|
||||
export type ProjectGiteaRepositoryDiagnostic = {
|
||||
projectKey: string;
|
||||
repositoryUrl: string;
|
||||
owner?: string;
|
||||
repo?: string;
|
||||
status: 'ok' | 'missing' | 'not_configured' | 'unavailable';
|
||||
message?: string;
|
||||
defaultBranch?: string;
|
||||
webUrl?: string;
|
||||
cloneUrl?: string;
|
||||
archived?: boolean;
|
||||
private?: boolean;
|
||||
empty?: boolean;
|
||||
branchCount?: number;
|
||||
tagCount?: number;
|
||||
latestCommit?: {
|
||||
sha: string;
|
||||
message: string;
|
||||
authorName?: string;
|
||||
authoredAt?: string;
|
||||
};
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
export type ProjectGiteaDiagnosticsSummary = {
|
||||
checkedAt: string;
|
||||
repositories: ProjectGiteaRepositoryDiagnostic[];
|
||||
};
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* 项目接口聚合项目配置、Gitea refs 和 Jenkins/Gitea 诊断结果。
|
||||
*/
|
||||
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { AuthGuard } from '../auth/auth.guard';
|
||||
@@ -5,6 +8,7 @@ import { AuthenticatedUser } from '../auth/auth.types';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import { PasswordChangeGuard } from '../auth/password-change.guard';
|
||||
import {
|
||||
ProjectGiteaDiagnosticsSummary,
|
||||
ProjectJenkinsDiagnosticsSummary,
|
||||
ProjectRefSummary,
|
||||
ProjectSummary,
|
||||
@@ -33,6 +37,14 @@ export class ProjectsController {
|
||||
return this.projectsService.getJenkinsDiagnostics(user);
|
||||
}
|
||||
|
||||
@Get('gitea-diagnostics')
|
||||
@ApiOkResponse({ description: '查询当前账号可访问项目的 Gitea 仓库可达性。' })
|
||||
async getGiteaDiagnostics(
|
||||
@CurrentUser() user: AuthenticatedUser,
|
||||
): Promise<ProjectGiteaDiagnosticsSummary> {
|
||||
return this.projectsService.getGiteaDiagnostics(user);
|
||||
}
|
||||
|
||||
@Get(':key')
|
||||
@ApiOkResponse({ description: '查询项目详情。' })
|
||||
async getProject(
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* 项目服务统一处理项目权限、Gitea 元数据读取和 Jenkins/Gitea 诊断聚合。
|
||||
*/
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { AuthenticatedUser } from '../auth/auth.types';
|
||||
@@ -11,6 +14,8 @@ import {
|
||||
import { JenkinsClient } from '../integrations/jenkins/jenkins.client';
|
||||
import { ProjectRepository } from './project.repository';
|
||||
import {
|
||||
ProjectGiteaDiagnosticsSummary,
|
||||
ProjectGiteaRepositoryDiagnostic,
|
||||
ProjectJenkinsDiagnosticsSummary,
|
||||
ProjectJenkinsJobDiagnostic,
|
||||
ProjectRefSummary,
|
||||
@@ -135,6 +140,40 @@ export class ProjectsService {
|
||||
};
|
||||
}
|
||||
|
||||
async getGiteaDiagnostics(
|
||||
user?: AuthenticatedUser,
|
||||
): Promise<ProjectGiteaDiagnosticsSummary> {
|
||||
const projects = await this.listProjects(user);
|
||||
const health = this.giteaClient.healthSummary();
|
||||
|
||||
if (health.status === 'not_configured') {
|
||||
return {
|
||||
checkedAt: new Date().toISOString(),
|
||||
repositories: projects.map((project) => {
|
||||
const repo = this.parseRepository(project);
|
||||
|
||||
return {
|
||||
projectKey: project.key,
|
||||
repositoryUrl: project.repositoryUrl,
|
||||
owner: repo?.owner,
|
||||
repo: repo?.name,
|
||||
status: 'not_configured',
|
||||
message: health.message,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const repositories = await Promise.all(
|
||||
projects.map((project) => this.getProjectGiteaDiagnostic(project)),
|
||||
);
|
||||
|
||||
return {
|
||||
checkedAt: new Date().toISOString(),
|
||||
repositories,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit 摘要只作为发布前辅助信息;单个摘要读取失败不能影响 refs 主流程。
|
||||
*/
|
||||
@@ -246,9 +285,121 @@ export class ProjectsService {
|
||||
}
|
||||
}
|
||||
|
||||
private async getProjectGiteaDiagnostic(
|
||||
project: ProjectSummary,
|
||||
): Promise<ProjectGiteaRepositoryDiagnostic> {
|
||||
const repo = this.parseRepository(project);
|
||||
|
||||
if (!repo) {
|
||||
return {
|
||||
projectKey: project.key,
|
||||
repositoryUrl: project.repositoryUrl,
|
||||
status: 'unavailable',
|
||||
message: 'Gitea 仓库地址无法解析',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const [repository, branches, tags] = await Promise.all([
|
||||
this.giteaClient.getRepository(repo.owner, repo.name),
|
||||
this.giteaClient.listBranches(repo.owner, repo.name),
|
||||
this.giteaClient.listTags(repo.owner, repo.name),
|
||||
]);
|
||||
const defaultBranch = repository.defaultBranch ?? project.defaultBranch;
|
||||
const latestCommit = await this.getRepositoryDefaultCommit(
|
||||
repo,
|
||||
defaultBranch,
|
||||
);
|
||||
|
||||
return {
|
||||
projectKey: project.key,
|
||||
repositoryUrl: project.repositoryUrl,
|
||||
owner: repository.owner,
|
||||
repo: repository.name,
|
||||
status: 'ok',
|
||||
message: this.giteaRepositoryMessage(repository),
|
||||
defaultBranch,
|
||||
webUrl: repository.webUrl ?? repo.webBaseUrl,
|
||||
cloneUrl: repository.cloneUrl,
|
||||
archived: repository.archived,
|
||||
private: repository.private,
|
||||
empty: repository.empty,
|
||||
branchCount: branches.length,
|
||||
tagCount: tags.length,
|
||||
latestCommit,
|
||||
updatedAt: repository.updatedAt,
|
||||
};
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof IntegrationRequestError &&
|
||||
error.details?.status === 404
|
||||
) {
|
||||
return {
|
||||
projectKey: project.key,
|
||||
repositoryUrl: project.repositoryUrl,
|
||||
owner: repo.owner,
|
||||
repo: repo.name,
|
||||
status: 'missing',
|
||||
message: 'Gitea 仓库不存在或当前 token 无权访问',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
projectKey: project.key,
|
||||
repositoryUrl: project.repositoryUrl,
|
||||
owner: repo.owner,
|
||||
repo: repo.name,
|
||||
status: 'unavailable',
|
||||
message: error instanceof Error ? error.message : 'Gitea 诊断失败',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认分支 commit 摘要只是展示增强;读取失败不影响仓库可达性诊断。
|
||||
*/
|
||||
private async getRepositoryDefaultCommit(
|
||||
repo: ParsedRepository,
|
||||
defaultBranch?: string,
|
||||
): Promise<ProjectGiteaRepositoryDiagnostic['latestCommit']> {
|
||||
if (!defaultBranch) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.giteaClient.getCommit(
|
||||
repo.owner,
|
||||
repo.name,
|
||||
defaultBranch,
|
||||
);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private giteaRepositoryMessage(
|
||||
repository: {
|
||||
archived?: boolean;
|
||||
empty?: boolean;
|
||||
},
|
||||
): string | undefined {
|
||||
if (repository.empty) {
|
||||
return 'Gitea 仓库为空';
|
||||
}
|
||||
|
||||
if (repository.archived) {
|
||||
return 'Gitea 仓库已归档';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private fallbackRefs(project: ProjectSummary): ProjectRefSummary {
|
||||
const branches = Array.from(
|
||||
new Set([project.defaultBranch, project.key === 'my-resume' ? 'master' : 'develop']),
|
||||
new Set([
|
||||
project.defaultBranch,
|
||||
project.key === 'my-resume' ? 'master' : 'develop',
|
||||
]),
|
||||
)
|
||||
.filter(Boolean)
|
||||
.map((name) => ({ name }));
|
||||
|
||||
Reference in New Issue
Block a user