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 { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { IntegrationRequestError, IntegrationUnavailableError } from '../../common/errors/integration-error';
|
import { IntegrationRequestError, IntegrationUnavailableError } from '../../common/errors/integration-error';
|
||||||
@@ -7,6 +10,7 @@ import {
|
|||||||
GiteaBranch,
|
GiteaBranch,
|
||||||
GiteaCommitSummary,
|
GiteaCommitSummary,
|
||||||
GiteaPullRequestSummary,
|
GiteaPullRequestSummary,
|
||||||
|
GiteaRepositoryInfo,
|
||||||
GiteaTag,
|
GiteaTag,
|
||||||
} from './gitea.types';
|
} 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(
|
async getCommit(
|
||||||
owner: string,
|
owner: string,
|
||||||
repo: string,
|
repo: string,
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Gitea API 的前端安全摘要类型,只保存仓库和提交元数据,不包含 token。
|
||||||
|
*/
|
||||||
export type GiteaBranch = {
|
export type GiteaBranch = {
|
||||||
name: string;
|
name: string;
|
||||||
commitSha: string;
|
commitSha: string;
|
||||||
@@ -27,3 +30,16 @@ export type GiteaPullRequestSummary = {
|
|||||||
baseRef?: string;
|
baseRef?: string;
|
||||||
authorName?: 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 = {
|
export type ProjectEnvironmentSummary = {
|
||||||
name: 'test' | 'production';
|
name: 'test' | 'production';
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@@ -68,3 +71,32 @@ export type ProjectJenkinsDiagnosticsSummary = {
|
|||||||
checkedAt: string;
|
checkedAt: string;
|
||||||
jobs: ProjectJenkinsJobDiagnostic[];
|
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 { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
||||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { AuthGuard } from '../auth/auth.guard';
|
import { AuthGuard } from '../auth/auth.guard';
|
||||||
@@ -5,6 +8,7 @@ import { AuthenticatedUser } from '../auth/auth.types';
|
|||||||
import { CurrentUser } from '../auth/current-user.decorator';
|
import { CurrentUser } from '../auth/current-user.decorator';
|
||||||
import { PasswordChangeGuard } from '../auth/password-change.guard';
|
import { PasswordChangeGuard } from '../auth/password-change.guard';
|
||||||
import {
|
import {
|
||||||
|
ProjectGiteaDiagnosticsSummary,
|
||||||
ProjectJenkinsDiagnosticsSummary,
|
ProjectJenkinsDiagnosticsSummary,
|
||||||
ProjectRefSummary,
|
ProjectRefSummary,
|
||||||
ProjectSummary,
|
ProjectSummary,
|
||||||
@@ -33,6 +37,14 @@ export class ProjectsController {
|
|||||||
return this.projectsService.getJenkinsDiagnostics(user);
|
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')
|
@Get(':key')
|
||||||
@ApiOkResponse({ description: '查询项目详情。' })
|
@ApiOkResponse({ description: '查询项目详情。' })
|
||||||
async getProject(
|
async getProject(
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* 项目服务统一处理项目权限、Gitea 元数据读取和 Jenkins/Gitea 诊断聚合。
|
||||||
|
*/
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { AuthService } from '../auth/auth.service';
|
import { AuthService } from '../auth/auth.service';
|
||||||
import { AuthenticatedUser } from '../auth/auth.types';
|
import { AuthenticatedUser } from '../auth/auth.types';
|
||||||
@@ -11,6 +14,8 @@ import {
|
|||||||
import { JenkinsClient } from '../integrations/jenkins/jenkins.client';
|
import { JenkinsClient } from '../integrations/jenkins/jenkins.client';
|
||||||
import { ProjectRepository } from './project.repository';
|
import { ProjectRepository } from './project.repository';
|
||||||
import {
|
import {
|
||||||
|
ProjectGiteaDiagnosticsSummary,
|
||||||
|
ProjectGiteaRepositoryDiagnostic,
|
||||||
ProjectJenkinsDiagnosticsSummary,
|
ProjectJenkinsDiagnosticsSummary,
|
||||||
ProjectJenkinsJobDiagnostic,
|
ProjectJenkinsJobDiagnostic,
|
||||||
ProjectRefSummary,
|
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 主流程。
|
* 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 {
|
private fallbackRefs(project: ProjectSummary): ProjectRefSummary {
|
||||||
const branches = Array.from(
|
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)
|
.filter(Boolean)
|
||||||
.map((name) => ({ name }));
|
.map((name) => ({ name }));
|
||||||
|
|||||||
Reference in New Issue
Block a user