feat: 增加项目 Jenkins 诊断接口
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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']),
|
||||
|
||||
Reference in New Issue
Block a user