feat: 增加项目 Jenkins 诊断接口

This commit is contained in:
湛兮
2026-06-12 04:12:22 +08:00
parent 08d48164fa
commit 555367a61f
6 changed files with 160 additions and 3 deletions
@@ -12,6 +12,7 @@ import { EnvConfig } from '../../config/env.schema';
import {
JenkinsBuildStatus,
JenkinsCancelResult,
JenkinsJobInfo,
JenkinsProgressiveText,
JenkinsQueueItem,
} from './jenkins.types';
@@ -121,6 +122,24 @@ export class JenkinsClient {
};
}
async getJobInfo(jobPath: string): Promise<JenkinsJobInfo> {
this.assertConfigured();
const response = await this.request(
`${this.jobPath(jobPath)}/api/json?tree=name,fullName,url,color,buildable,lastBuild[number,result,building,url]`,
);
const payload = (await response.json()) as Omit<JenkinsJobInfo, 'jobPath'>;
return {
jobPath,
name: payload.name,
fullName: payload.fullName,
url: payload.url,
color: payload.color,
buildable: payload.buildable,
lastBuild: payload.lastBuild,
};
}
async getProgressiveText(
jobPath: string,
buildNumber: number,
+15
View File
@@ -12,6 +12,21 @@ export type JenkinsBuildStatus = {
url?: string;
};
export type JenkinsJobInfo = {
jobPath: string;
name?: string;
fullName?: string;
url?: string;
color?: string;
buildable?: boolean;
lastBuild?: {
number?: number;
result?: JenkinsBuildStatus['result'];
building?: boolean;
url?: string;
};
};
export type JenkinsProgressiveText = {
text: string;
nextStart: number;
+21
View File
@@ -47,3 +47,24 @@ export type ProjectRefSummary = {
branches: ProjectRefItemSummary[];
tags: ProjectRefItemSummary[];
};
export type ProjectJenkinsJobDiagnostic = {
projectKey: string;
environment: ProjectEnvironmentSummary['name'];
jenkinsJobPath: string;
status: 'ok' | 'missing' | 'not_configured' | 'unavailable';
message?: string;
buildable?: boolean;
url?: string;
lastBuild?: {
number?: number;
result?: 'SUCCESS' | 'FAILURE' | 'ABORTED' | 'UNSTABLE';
building?: boolean;
url?: string;
};
};
export type ProjectJenkinsDiagnosticsSummary = {
checkedAt: string;
jobs: ProjectJenkinsJobDiagnostic[];
};
+13 -1
View File
@@ -4,7 +4,11 @@ import { AuthGuard } from '../auth/auth.guard';
import { AuthenticatedUser } from '../auth/auth.types';
import { CurrentUser } from '../auth/current-user.decorator';
import { PasswordChangeGuard } from '../auth/password-change.guard';
import { ProjectRefSummary, ProjectSummary } from './project.types';
import {
ProjectJenkinsDiagnosticsSummary,
ProjectRefSummary,
ProjectSummary,
} from './project.types';
import { ProjectsService } from './projects.service';
@ApiTags('项目')
@@ -21,6 +25,14 @@ export class ProjectsController {
return this.projectsService.listProjects(user);
}
@Get('jenkins-diagnostics')
@ApiOkResponse({ description: '查询当前账号可访问项目的 Jenkins job 可达性。' })
async getJenkinsDiagnostics(
@CurrentUser() user: AuthenticatedUser,
): Promise<ProjectJenkinsDiagnosticsSummary> {
return this.projectsService.getJenkinsDiagnostics(user);
}
@Get(':key')
@ApiOkResponse({ description: '查询项目详情。' })
async getProject(
+2 -1
View File
@@ -1,12 +1,13 @@
import { Module } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module';
import { GiteaModule } from '../integrations/gitea/gitea.module';
import { JenkinsModule } from '../integrations/jenkins/jenkins.module';
import { ProjectRepository } from './project.repository';
import { ProjectsController } from './projects.controller';
import { ProjectsService } from './projects.service';
@Module({
imports: [AuthModule, GiteaModule],
imports: [AuthModule, GiteaModule, JenkinsModule],
controllers: [ProjectsController],
providers: [ProjectRepository, ProjectsService],
exports: [ProjectRepository, ProjectsService],
+90 -1
View File
@@ -2,13 +2,20 @@ import { Injectable } from '@nestjs/common';
import { AuthService } from '../auth/auth.service';
import { AuthenticatedUser } from '../auth/auth.types';
import { AppError } from '../common/errors/app-error';
import { IntegrationRequestError } from '../common/errors/integration-error';
import { GiteaClient } from '../integrations/gitea/gitea.client';
import {
GiteaCommitSummary,
GiteaPullRequestSummary,
} from '../integrations/gitea/gitea.types';
import { JenkinsClient } from '../integrations/jenkins/jenkins.client';
import { ProjectRepository } from './project.repository';
import { ProjectRefSummary, ProjectSummary } from './project.types';
import {
ProjectJenkinsDiagnosticsSummary,
ProjectJenkinsJobDiagnostic,
ProjectRefSummary,
ProjectSummary,
} from './project.types';
type ParsedRepository = {
owner: string;
@@ -21,6 +28,7 @@ export class ProjectsService {
constructor(
private readonly projectRepository: ProjectRepository,
private readonly giteaClient: GiteaClient,
private readonly jenkinsClient: JenkinsClient,
private readonly authService: AuthService,
) {}
@@ -88,6 +96,45 @@ export class ProjectsService {
return this.fallbackRefs(project);
}
async getJenkinsDiagnostics(
user?: AuthenticatedUser,
): Promise<ProjectJenkinsDiagnosticsSummary> {
const projects = await this.listProjects(user);
const health = this.jenkinsClient.healthSummary();
if (health.status === 'not_configured') {
return {
checkedAt: new Date().toISOString(),
jobs: projects.flatMap((project) =>
project.environments.map((environment) => ({
projectKey: project.key,
environment: environment.name,
jenkinsJobPath: environment.jenkinsJobPath,
status: 'not_configured',
message: health.message,
})),
),
};
}
const jobs = await Promise.all(
projects.flatMap((project) =>
project.environments.map((environment) =>
this.getEnvironmentJenkinsDiagnostic(
project.key,
environment.name,
environment.jenkinsJobPath,
),
),
),
);
return {
checkedAt: new Date().toISOString(),
jobs,
};
}
/**
* Commit 摘要只作为发布前辅助信息;单个摘要读取失败不能影响 refs 主流程。
*/
@@ -157,6 +204,48 @@ export class ProjectsService {
}
}
private async getEnvironmentJenkinsDiagnostic(
projectKey: string,
environment: ProjectJenkinsJobDiagnostic['environment'],
jenkinsJobPath: string,
): Promise<ProjectJenkinsJobDiagnostic> {
try {
const info = await this.jenkinsClient.getJobInfo(jenkinsJobPath);
return {
projectKey,
environment,
jenkinsJobPath,
status: info.buildable === false ? 'unavailable' : 'ok',
message: info.buildable === false ? 'Jenkins job 当前不可构建' : undefined,
buildable: info.buildable,
url: info.url,
lastBuild: info.lastBuild,
};
} catch (error) {
if (
error instanceof IntegrationRequestError &&
error.details?.status === 404
) {
return {
projectKey,
environment,
jenkinsJobPath,
status: 'missing',
message: 'Jenkins job 不存在',
};
}
return {
projectKey,
environment,
jenkinsJobPath,
status: 'unavailable',
message: error instanceof Error ? error.message : 'Jenkins 诊断失败',
};
}
}
private fallbackRefs(project: ProjectSummary): ProjectRefSummary {
const branches = Array.from(
new Set([project.defaultBranch, project.key === 'my-resume' ? 'master' : 'develop']),