feat: 展示项目 Gitea 仓库诊断

- devopsApi: 接入项目 Gitea 诊断接口
- platform store: 回填仓库可达性和默认分支 commit
- ProjectsView: 展示 Gitea 状态、分支 tag 数量和 commit 摘要
This commit is contained in:
湛兮
2026-06-12 04:38:51 +08:00
parent 4e15547662
commit cb8519d3f9
4 changed files with 252 additions and 7 deletions
+68
View File
@@ -29,6 +29,8 @@ import type {
PipelineStep,
PlatformMessageSummary,
ProjectConfig,
ProjectGiteaDiagnosticsSummary,
ProjectGiteaRepositoryDiagnostic,
ProjectJenkinsDiagnosticsSummary,
ProjectJenkinsJobDiagnostic,
ProjectRefs,
@@ -359,6 +361,14 @@ export const devopsApi = {
return sanitizeProjectJenkinsDiagnostics(data)
},
async getProjectGiteaDiagnostics(): Promise<ProjectGiteaDiagnosticsSummary> {
const { data } = await http.get<ProjectGiteaDiagnosticsSummary>(
'/projects/gitea-diagnostics',
)
lastDataSource = 'backend'
return sanitizeProjectGiteaDiagnostics(data)
},
async getProjectRefs(projectKey: string): Promise<ProjectRefs> {
const { data } = await http.get<ProjectRefs>(`/projects/${projectKey}/refs`)
lastDataSource = 'backend'
@@ -786,6 +796,8 @@ function toProjectConfigs(backendProjects: BackendProject[]): ProjectConfig[] {
serviceType: serviceTypeLabel(project.key),
defaultBranch: project.defaultBranch,
tagPolicy: tagPolicyLabel(project.environments),
giteaStatus: 'warning',
giteaMessage: '尚未执行 Gitea 诊断',
environments: project.environments.map((environment) => {
return {
name: environment.name,
@@ -812,6 +824,62 @@ function toProjectConfigs(backendProjects: BackendProject[]): ProjectConfig[] {
})
}
function sanitizeProjectGiteaDiagnostics(
value: ProjectGiteaDiagnosticsSummary,
): ProjectGiteaDiagnosticsSummary {
return {
checkedAt: safeText(value.checkedAt, new Date().toISOString()),
repositories: Array.isArray(value.repositories)
? value.repositories.map(sanitizeProjectGiteaRepositoryDiagnostic)
: [],
}
}
function sanitizeProjectGiteaRepositoryDiagnostic(
value: ProjectGiteaRepositoryDiagnostic,
): ProjectGiteaRepositoryDiagnostic {
return {
projectKey: safeText(value.projectKey, 'unknown-project'),
repositoryUrl: safeText(value.repositoryUrl, ''),
owner: optionalSafeText(value.owner),
repo: optionalSafeText(value.repo),
status: toProjectGiteaDiagnosticStatus(value.status),
message: optionalSafeText(value.message),
defaultBranch: optionalSafeText(value.defaultBranch),
webUrl: optionalSafeText(value.webUrl),
cloneUrl: optionalSafeText(value.cloneUrl),
archived: typeof value.archived === 'boolean' ? value.archived : undefined,
private: typeof value.private === 'boolean' ? value.private : undefined,
empty: typeof value.empty === 'boolean' ? value.empty : undefined,
branchCount: Number.isFinite(value.branchCount) ? value.branchCount : undefined,
tagCount: Number.isFinite(value.tagCount) ? value.tagCount : undefined,
latestCommit: value.latestCommit
? {
sha: safeText(value.latestCommit.sha, ''),
message: safeText(value.latestCommit.message, ''),
authorName: optionalSafeText(value.latestCommit.authorName),
authoredAt: optionalSafeText(value.latestCommit.authoredAt),
}
: undefined,
updatedAt: optionalSafeText(value.updatedAt),
}
}
function toProjectGiteaDiagnosticStatus(
value: ProjectGiteaRepositoryDiagnostic['status'],
): ProjectGiteaRepositoryDiagnostic['status'] {
if (
value === 'ok' ||
value === 'missing' ||
value === 'not_configured' ||
value === 'unavailable'
) {
return value
}
return 'unavailable'
}
function sanitizeProjectJenkinsDiagnostics(
value: ProjectJenkinsDiagnosticsSummary,
): ProjectJenkinsDiagnosticsSummary {
+82
View File
@@ -12,6 +12,7 @@ import type {
IntegrationStatus,
IntegrationConfigStatus,
ProjectConfig,
ProjectGiteaDiagnosticsSummary,
ProjectJenkinsDiagnosticsSummary,
ProjectRefs,
JenkinsSyncSummary,
@@ -34,6 +35,7 @@ export const usePlatformStore = defineStore('platform', () => {
const auditLogs = ref<AuditLogEntry[]>([])
const notificationOutbox = ref<NotificationOutboxMessage[]>([])
const projectJenkinsDiagnostics = ref<ProjectJenkinsDiagnosticsSummary | null>(null)
const projectGiteaDiagnostics = ref<ProjectGiteaDiagnosticsSummary | null>(null)
const integrationConfig = ref<IntegrationConfigStatus>({
checkedAt: '',
integrations: [],
@@ -75,6 +77,7 @@ export const usePlatformStore = defineStore('platform', () => {
releaseProcessXml.value = processXml
auditLogs.value = recentAuditLogs
notificationOutbox.value = recentNotificationOutbox
applyProjectGiteaDiagnostics()
applyProjectJenkinsDiagnostics()
dataSource.value = devopsApi.getLastDataSource()
} finally {
@@ -135,6 +138,14 @@ export const usePlatformStore = defineStore('platform', () => {
return diagnostics
}
async function loadProjectGiteaDiagnostics(): Promise<ProjectGiteaDiagnosticsSummary> {
const diagnostics = await devopsApi.getProjectGiteaDiagnostics()
projectGiteaDiagnostics.value = diagnostics
applyProjectGiteaDiagnostics()
dataSource.value = devopsApi.getLastDataSource()
return diagnostics
}
function applyProjectJenkinsDiagnostics() {
const diagnostics = projectJenkinsDiagnostics.value
@@ -177,6 +188,43 @@ export const usePlatformStore = defineStore('platform', () => {
})
}
function applyProjectGiteaDiagnostics() {
const diagnostics = projectGiteaDiagnostics.value
if (!diagnostics) {
return
}
const repositoryByProject = new Map(
diagnostics.repositories.map((repository) => [repository.projectKey, repository]),
)
projects.value = projects.value.map((project) => {
const diagnostic = repositoryByProject.get(project.key)
if (!diagnostic) {
return project
}
const giteaStatus = giteaDiagnosticState(diagnostic.status)
return {
...project,
giteaStatus,
giteaMessage: diagnostic.message || giteaDiagnosticMessage(diagnostic.status),
giteaWebUrl: diagnostic.webUrl,
giteaDefaultBranch: diagnostic.defaultBranch,
giteaBranchCount: diagnostic.branchCount,
giteaTagCount: diagnostic.tagCount,
giteaLatestCommit: diagnostic.latestCommit,
integrations: {
...project.integrations,
gitea: giteaStatus,
},
}
})
}
async function loadAuditLogs() {
auditLogs.value = await devopsApi.getAuditLogs()
dataSource.value = devopsApi.getLastDataSource()
@@ -257,6 +305,7 @@ export const usePlatformStore = defineStore('platform', () => {
auditLogs,
notificationOutbox,
projectJenkinsDiagnostics,
projectGiteaDiagnostics,
integrationConfig,
releaseProcessXml,
projectRefs,
@@ -270,6 +319,7 @@ export const usePlatformStore = defineStore('platform', () => {
syncJenkinsRuns,
loadProjectRefs,
loadProjectJenkinsDiagnostics,
loadProjectGiteaDiagnostics,
loadAuditLogs,
loadNotificationOutbox,
retryNotificationOutboxMessage,
@@ -308,6 +358,38 @@ function jenkinsDiagnosticMessage(
return 'Jenkins job 不可用'
}
function giteaDiagnosticState(
status: ProjectGiteaDiagnosticsSummary['repositories'][number]['status'],
) {
if (status === 'ok') {
return 'healthy' as const
}
if (status === 'not_configured') {
return 'warning' as const
}
return 'error' as const
}
function giteaDiagnosticMessage(
status: ProjectGiteaDiagnosticsSummary['repositories'][number]['status'],
): string {
if (status === 'ok') {
return 'Gitea 仓库可达'
}
if (status === 'missing') {
return 'Gitea 仓库不存在或无权访问'
}
if (status === 'not_configured') {
return 'Gitea 集成未配置'
}
return 'Gitea 仓库不可用'
}
function projectEnvironmentKey(
projectKey: string,
environment: ReleaseRun['environment'],
+31
View File
@@ -208,6 +208,13 @@ export interface ProjectConfig {
serviceType: string
defaultBranch: string
tagPolicy: string
giteaStatus: HealthState
giteaMessage?: string
giteaWebUrl?: string
giteaDefaultBranch?: string
giteaBranchCount?: number
giteaTagCount?: number
giteaLatestCommit?: ProjectRefCommit
environments: EnvironmentStatus[]
integrations: {
jenkins: HealthState
@@ -251,6 +258,30 @@ export interface ProjectRefCommit {
authoredAt?: string
}
export interface 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?: ProjectRefCommit
updatedAt?: string
}
export interface ProjectGiteaDiagnosticsSummary {
checkedAt: string
repositories: ProjectGiteaRepositoryDiagnostic[]
}
export interface ProjectRefPullRequest {
number: number
title: string
+71 -7
View File
@@ -1,24 +1,27 @@
<script setup lang="ts">
/**
* 项目配置页展示项目仓库、发布环境以及真实 Jenkins job 诊断状态。
* 项目配置页展示项目仓库、发布环境、Gitea 仓库诊断和真实 Jenkins job 诊断状态。
*/
import { onMounted, ref } from 'vue'
import StatusPill from '../components/StatusPill.vue'
import { usePlatformStore } from '../stores/platform'
import type { EnvironmentStatus } from '../types/devops'
import type { EnvironmentStatus, ProjectConfig } from '../types/devops'
const platform = usePlatformStore()
const diagnosticsLoading = ref(false)
onMounted(() => {
void refreshJenkinsDiagnostics()
void refreshProjectDiagnostics()
})
async function refreshJenkinsDiagnostics() {
async function refreshProjectDiagnostics() {
diagnosticsLoading.value = true
try {
await platform.loadProjectJenkinsDiagnostics()
await Promise.all([
platform.loadProjectJenkinsDiagnostics(),
platform.loadProjectGiteaDiagnostics(),
])
} finally {
diagnosticsLoading.value = false
}
@@ -35,6 +38,28 @@ function lastBuildText(env: EnvironmentStatus): string {
return `#${env.jenkinsLastBuild.number} ${env.jenkinsLastBuild.result || 'UNKNOWN'}`
}
function commitText(project: ProjectConfig): string {
const commit = project.giteaLatestCommit
if (!commit?.sha) {
return '-'
}
const title = commit.message.split('\n')[0] || '无提交说明'
return `${commit.sha.slice(0, 8)} · ${title}`
}
function branchTagText(project: ProjectConfig): string {
if (project.giteaBranchCount === undefined && project.giteaTagCount === undefined) {
return '尚未诊断'
}
const branchCount = project.giteaBranchCount ?? 0
const tagCount = project.giteaTagCount ?? 0
return `${branchCount} branches · ${tagCount} tags`
}
</script>
<template>
@@ -54,9 +79,9 @@ function lastBuildText(env: EnvironmentStatus): string {
size="small"
plain
:loading="diagnosticsLoading"
@click="refreshJenkinsDiagnostics"
@click="refreshProjectDiagnostics"
>
刷新 Jenkins 诊断
刷新项目诊断
</ElButton>
</div>
<div class="panel-body">
@@ -64,6 +89,37 @@ function lastBuildText(env: EnvironmentStatus): string {
<ElTableColumn type="expand">
<template #default="{ row }">
<div class="env-config">
<div class="project-diagnostic-card">
<div class="env-config-head">
<strong>Gitea 仓库诊断</strong>
<StatusPill :state="row.giteaStatus" />
</div>
<dl>
<dt>仓库状态</dt>
<dd>
<StatusPill :state="row.giteaStatus" />
<span class="diagnostic-message">{{ row.giteaMessage }}</span>
</dd>
<dt>公开地址</dt>
<dd>
<a
v-if="row.giteaWebUrl"
:href="row.giteaWebUrl"
target="_blank"
rel="noreferrer"
>
{{ row.giteaWebUrl }}
</a>
<span v-else class="mono">{{ row.repository }}</span>
</dd>
<dt>默认分支</dt>
<dd>{{ row.giteaDefaultBranch || row.defaultBranch }}</dd>
<dt>分支 / Tag</dt>
<dd>{{ branchTagText(row) }}</dd>
<dt>默认分支 commit</dt>
<dd class="mono">{{ commitText(row) }}</dd>
</dl>
</div>
<div v-for="env in row.environments" :key="env.name" class="env-config-card">
<div class="env-config-head">
<strong>{{ env.label }}</strong>
@@ -157,6 +213,14 @@ function lastBuildText(env: EnvironmentStatus): string {
background: #ffffff;
}
.project-diagnostic-card {
grid-column: 1 / -1;
padding: 14px;
border: 1px solid #d7e5ff;
border-radius: 8px;
background: #ffffff;
}
.env-config-head {
display: flex;
align-items: center;