feat: 展示项目 Jenkins 诊断状态

This commit is contained in:
湛兮
2026-06-12 04:12:22 +08:00
parent 1dedafc7bc
commit cd3fdb4a5a
4 changed files with 231 additions and 1 deletions
+65
View File
@@ -29,6 +29,8 @@ import type {
PipelineStep,
PlatformMessageSummary,
ProjectConfig,
ProjectJenkinsDiagnosticsSummary,
ProjectJenkinsJobDiagnostic,
ProjectRefs,
ReleaseRun,
ReleaseStatus,
@@ -349,6 +351,14 @@ export const devopsApi = {
return toProjectConfigs(await fetchProjects())
},
async getProjectJenkinsDiagnostics(): Promise<ProjectJenkinsDiagnosticsSummary> {
const { data } = await http.get<ProjectJenkinsDiagnosticsSummary>(
'/projects/jenkins-diagnostics',
)
lastDataSource = 'backend'
return sanitizeProjectJenkinsDiagnostics(data)
},
async getProjectRefs(projectKey: string): Promise<ProjectRefs> {
const { data } = await http.get<ProjectRefs>(`/projects/${projectKey}/refs`)
lastDataSource = 'backend'
@@ -787,6 +797,8 @@ function toProjectConfigs(backendProjects: BackendProject[]): ProjectConfig[] {
lastReleasedAt: '-',
lastActor: '-',
jenkinsJob: environment.jenkinsJobPath,
jenkinsStatus: 'warning' as const,
jenkinsMessage: '尚未执行 Jenkins 诊断',
wecomRobot: '后端通知配置',
}
}),
@@ -800,6 +812,59 @@ function toProjectConfigs(backendProjects: BackendProject[]): ProjectConfig[] {
})
}
function sanitizeProjectJenkinsDiagnostics(
value: ProjectJenkinsDiagnosticsSummary,
): ProjectJenkinsDiagnosticsSummary {
return {
checkedAt: safeText(value.checkedAt, new Date().toISOString()),
jobs: Array.isArray(value.jobs)
? value.jobs.map(sanitizeProjectJenkinsJobDiagnostic)
: [],
}
}
function sanitizeProjectJenkinsJobDiagnostic(
value: ProjectJenkinsJobDiagnostic,
): ProjectJenkinsJobDiagnostic {
return {
projectKey: safeText(value.projectKey, 'unknown-project'),
environment: toEnvironmentName(value.environment),
jenkinsJobPath: safeText(value.jenkinsJobPath, ''),
status: toProjectJenkinsDiagnosticStatus(value.status),
message: optionalSafeText(value.message),
buildable: typeof value.buildable === 'boolean' ? value.buildable : undefined,
url: optionalSafeText(value.url),
lastBuild: value.lastBuild
? {
number: Number.isFinite(value.lastBuild.number)
? value.lastBuild.number
: undefined,
result: value.lastBuild.result,
building:
typeof value.lastBuild.building === 'boolean'
? value.lastBuild.building
: undefined,
url: optionalSafeText(value.lastBuild.url),
}
: undefined,
}
}
function toProjectJenkinsDiagnosticStatus(
value: ProjectJenkinsJobDiagnostic['status'],
): ProjectJenkinsJobDiagnostic['status'] {
if (
value === 'ok' ||
value === 'missing' ||
value === 'not_configured' ||
value === 'unavailable'
) {
return value
}
return 'unavailable'
}
function toReleaseRun(run: BackendDeployRun, projectList: ProjectConfig[]): ReleaseRun {
const projectKey = run.projectKey || 'unknown-project'
const project = projectList.find((item) => item.key === projectKey)
+73
View File
@@ -12,6 +12,7 @@ import type {
IntegrationStatus,
IntegrationConfigStatus,
ProjectConfig,
ProjectJenkinsDiagnosticsSummary,
ProjectRefs,
JenkinsSyncSummary,
NotificationOutboxMessage,
@@ -123,6 +124,45 @@ export const usePlatformStore = defineStore('platform', () => {
return refs
}
async function loadProjectJenkinsDiagnostics(): Promise<ProjectJenkinsDiagnosticsSummary> {
const diagnostics = await devopsApi.getProjectJenkinsDiagnostics()
const jobByEnvironment = new Map(
diagnostics.jobs.map((job) => [`${job.projectKey}:${job.environment}`, job]),
)
projects.value = projects.value.map((project) => {
const environments = project.environments.map((environment) => {
const diagnostic = jobByEnvironment.get(`${project.key}:${environment.name}`)
if (!diagnostic) {
return environment
}
return {
...environment,
jenkinsJob: diagnostic.jenkinsJobPath || environment.jenkinsJob,
jenkinsStatus: jenkinsDiagnosticState(diagnostic.status),
jenkinsMessage: diagnostic.message || jenkinsDiagnosticMessage(diagnostic.status),
jenkinsUrl: diagnostic.url,
jenkinsLastBuild: diagnostic.lastBuild,
}
})
const hasError = environments.some((environment) => environment.jenkinsStatus === 'error')
const hasWarning = environments.some((environment) => environment.jenkinsStatus === 'warning')
return {
...project,
environments,
integrations: {
...project.integrations,
jenkins: hasError ? 'error' : hasWarning ? 'warning' : 'healthy',
},
}
})
dataSource.value = devopsApi.getLastDataSource()
return diagnostics
}
async function loadAuditLogs() {
auditLogs.value = await devopsApi.getAuditLogs()
dataSource.value = devopsApi.getLastDataSource()
@@ -171,8 +211,41 @@ export const usePlatformStore = defineStore('platform', () => {
syncJenkinsRun,
syncJenkinsRuns,
loadProjectRefs,
loadProjectJenkinsDiagnostics,
loadAuditLogs,
loadNotificationOutbox,
retryNotificationOutboxMessage,
}
})
function jenkinsDiagnosticState(
status: ProjectJenkinsDiagnosticsSummary['jobs'][number]['status'],
) {
if (status === 'ok') {
return 'healthy' as const
}
if (status === 'not_configured') {
return 'warning' as const
}
return 'error' as const
}
function jenkinsDiagnosticMessage(
status: ProjectJenkinsDiagnosticsSummary['jobs'][number]['status'],
): string {
if (status === 'ok') {
return 'Jenkins job 可达'
}
if (status === 'missing') {
return 'Jenkins job 不存在'
}
if (status === 'not_configured') {
return 'Jenkins 集成未配置'
}
return 'Jenkins job 不可用'
}
+30
View File
@@ -187,6 +187,15 @@ export interface EnvironmentStatus {
lastReleasedAt: string
lastActor: string
jenkinsJob: string
jenkinsStatus: HealthState
jenkinsMessage?: string
jenkinsUrl?: string
jenkinsLastBuild?: {
number?: number
result?: 'SUCCESS' | 'FAILURE' | 'ABORTED' | 'UNSTABLE'
building?: boolean
url?: string
}
wecomRobot: string
}
@@ -214,6 +223,27 @@ export interface ProjectRefs {
tags: ProjectRefItem[]
}
export interface ProjectJenkinsJobDiagnostic {
projectKey: string
environment: EnvironmentName
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 interface ProjectJenkinsDiagnosticsSummary {
checkedAt: string
jobs: ProjectJenkinsJobDiagnostic[]
}
export interface ProjectRefCommit {
sha: string
message: string
+63 -1
View File
@@ -1,8 +1,40 @@
<script setup lang="ts">
/**
* 项目配置页展示项目仓库、发布环境以及真实 Jenkins job 诊断状态。
*/
import { onMounted, ref } from 'vue'
import StatusPill from '../components/StatusPill.vue'
import { usePlatformStore } from '../stores/platform'
import type { EnvironmentStatus } from '../types/devops'
const platform = usePlatformStore()
const diagnosticsLoading = ref(false)
onMounted(() => {
void refreshJenkinsDiagnostics()
})
async function refreshJenkinsDiagnostics() {
diagnosticsLoading.value = true
try {
await platform.loadProjectJenkinsDiagnostics()
} finally {
diagnosticsLoading.value = false
}
}
function lastBuildText(env: EnvironmentStatus): string {
if (!env.jenkinsLastBuild?.number) {
return '-'
}
if (env.jenkinsLastBuild.building) {
return `#${env.jenkinsLastBuild.number} 运行中`
}
return `#${env.jenkinsLastBuild.number} ${env.jenkinsLastBuild.result || 'UNKNOWN'}`
}
</script>
<template>
@@ -18,7 +50,14 @@ const platform = usePlatformStore()
<section class="panel">
<div class="panel-header">
<h2>项目清单</h2>
<span>由后端 projects 接口返回</span>
<ElButton
size="small"
plain
:loading="diagnosticsLoading"
@click="refreshJenkinsDiagnostics"
>
刷新 Jenkins 诊断
</ElButton>
</div>
<div class="panel-body">
<ElTable :data="platform.projects" row-key="key">
@@ -35,6 +74,23 @@ const platform = usePlatformStore()
<dd>{{ env.branchPolicy }}</dd>
<dt>Jenkins Job</dt>
<dd class="mono">{{ env.jenkinsJob }}</dd>
<dt>Job 状态</dt>
<dd>
<StatusPill :state="env.jenkinsStatus" />
<span class="diagnostic-message">{{ env.jenkinsMessage }}</span>
</dd>
<dt>最近构建</dt>
<dd>
<a
v-if="env.jenkinsLastBuild?.url"
:href="env.jenkinsLastBuild.url"
target="_blank"
rel="noreferrer"
>
{{ lastBuildText(env) }}
</a>
<span v-else>{{ lastBuildText(env) }}</span>
</dd>
<dt>通知通道</dt>
<dd>{{ env.wecomRobot }}</dd>
<dt>最近发布</dt>
@@ -116,6 +172,12 @@ dd {
gap: 6px;
}
.diagnostic-message {
margin-left: 8px;
color: #596578;
font-size: 12px;
}
@media (max-width: 760px) {
.env-config {
grid-template-columns: 1fr;