feat: 增加项目 Jenkins 诊断接口
This commit is contained in:
@@ -12,6 +12,7 @@ import { EnvConfig } from '../../config/env.schema';
|
|||||||
import {
|
import {
|
||||||
JenkinsBuildStatus,
|
JenkinsBuildStatus,
|
||||||
JenkinsCancelResult,
|
JenkinsCancelResult,
|
||||||
|
JenkinsJobInfo,
|
||||||
JenkinsProgressiveText,
|
JenkinsProgressiveText,
|
||||||
JenkinsQueueItem,
|
JenkinsQueueItem,
|
||||||
} from './jenkins.types';
|
} 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(
|
async getProgressiveText(
|
||||||
jobPath: string,
|
jobPath: string,
|
||||||
buildNumber: number,
|
buildNumber: number,
|
||||||
|
|||||||
@@ -12,6 +12,21 @@ export type JenkinsBuildStatus = {
|
|||||||
url?: string;
|
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 = {
|
export type JenkinsProgressiveText = {
|
||||||
text: string;
|
text: string;
|
||||||
nextStart: number;
|
nextStart: number;
|
||||||
|
|||||||
@@ -47,3 +47,24 @@ export type ProjectRefSummary = {
|
|||||||
branches: ProjectRefItemSummary[];
|
branches: ProjectRefItemSummary[];
|
||||||
tags: 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 { 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 { ProjectRefSummary, ProjectSummary } from './project.types';
|
import {
|
||||||
|
ProjectJenkinsDiagnosticsSummary,
|
||||||
|
ProjectRefSummary,
|
||||||
|
ProjectSummary,
|
||||||
|
} from './project.types';
|
||||||
import { ProjectsService } from './projects.service';
|
import { ProjectsService } from './projects.service';
|
||||||
|
|
||||||
@ApiTags('项目')
|
@ApiTags('项目')
|
||||||
@@ -21,6 +25,14 @@ export class ProjectsController {
|
|||||||
return this.projectsService.listProjects(user);
|
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')
|
@Get(':key')
|
||||||
@ApiOkResponse({ description: '查询项目详情。' })
|
@ApiOkResponse({ description: '查询项目详情。' })
|
||||||
async getProject(
|
async getProject(
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
import { GiteaModule } from '../integrations/gitea/gitea.module';
|
import { GiteaModule } from '../integrations/gitea/gitea.module';
|
||||||
|
import { JenkinsModule } from '../integrations/jenkins/jenkins.module';
|
||||||
import { ProjectRepository } from './project.repository';
|
import { ProjectRepository } from './project.repository';
|
||||||
import { ProjectsController } from './projects.controller';
|
import { ProjectsController } from './projects.controller';
|
||||||
import { ProjectsService } from './projects.service';
|
import { ProjectsService } from './projects.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AuthModule, GiteaModule],
|
imports: [AuthModule, GiteaModule, JenkinsModule],
|
||||||
controllers: [ProjectsController],
|
controllers: [ProjectsController],
|
||||||
providers: [ProjectRepository, ProjectsService],
|
providers: [ProjectRepository, ProjectsService],
|
||||||
exports: [ProjectRepository, ProjectsService],
|
exports: [ProjectRepository, ProjectsService],
|
||||||
|
|||||||
@@ -2,13 +2,20 @@ 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';
|
||||||
import { AppError } from '../common/errors/app-error';
|
import { AppError } from '../common/errors/app-error';
|
||||||
|
import { IntegrationRequestError } from '../common/errors/integration-error';
|
||||||
import { GiteaClient } from '../integrations/gitea/gitea.client';
|
import { GiteaClient } from '../integrations/gitea/gitea.client';
|
||||||
import {
|
import {
|
||||||
GiteaCommitSummary,
|
GiteaCommitSummary,
|
||||||
GiteaPullRequestSummary,
|
GiteaPullRequestSummary,
|
||||||
} from '../integrations/gitea/gitea.types';
|
} from '../integrations/gitea/gitea.types';
|
||||||
|
import { JenkinsClient } from '../integrations/jenkins/jenkins.client';
|
||||||
import { ProjectRepository } from './project.repository';
|
import { ProjectRepository } from './project.repository';
|
||||||
import { ProjectRefSummary, ProjectSummary } from './project.types';
|
import {
|
||||||
|
ProjectJenkinsDiagnosticsSummary,
|
||||||
|
ProjectJenkinsJobDiagnostic,
|
||||||
|
ProjectRefSummary,
|
||||||
|
ProjectSummary,
|
||||||
|
} from './project.types';
|
||||||
|
|
||||||
type ParsedRepository = {
|
type ParsedRepository = {
|
||||||
owner: string;
|
owner: string;
|
||||||
@@ -21,6 +28,7 @@ export class ProjectsService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly projectRepository: ProjectRepository,
|
private readonly projectRepository: ProjectRepository,
|
||||||
private readonly giteaClient: GiteaClient,
|
private readonly giteaClient: GiteaClient,
|
||||||
|
private readonly jenkinsClient: JenkinsClient,
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -88,6 +96,45 @@ export class ProjectsService {
|
|||||||
return this.fallbackRefs(project);
|
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 主流程。
|
* 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 {
|
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']),
|
||||||
|
|||||||
Reference in New Issue
Block a user