feat: 展示项目 Gitea 仓库诊断
- devopsApi: 接入项目 Gitea 诊断接口 - platform store: 回填仓库可达性和默认分支 commit - ProjectsView: 展示 Gitea 状态、分支 tag 数量和 commit 摘要
This commit is contained in:
@@ -29,6 +29,8 @@ import type {
|
|||||||
PipelineStep,
|
PipelineStep,
|
||||||
PlatformMessageSummary,
|
PlatformMessageSummary,
|
||||||
ProjectConfig,
|
ProjectConfig,
|
||||||
|
ProjectGiteaDiagnosticsSummary,
|
||||||
|
ProjectGiteaRepositoryDiagnostic,
|
||||||
ProjectJenkinsDiagnosticsSummary,
|
ProjectJenkinsDiagnosticsSummary,
|
||||||
ProjectJenkinsJobDiagnostic,
|
ProjectJenkinsJobDiagnostic,
|
||||||
ProjectRefs,
|
ProjectRefs,
|
||||||
@@ -359,6 +361,14 @@ export const devopsApi = {
|
|||||||
return sanitizeProjectJenkinsDiagnostics(data)
|
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> {
|
async getProjectRefs(projectKey: string): Promise<ProjectRefs> {
|
||||||
const { data } = await http.get<ProjectRefs>(`/projects/${projectKey}/refs`)
|
const { data } = await http.get<ProjectRefs>(`/projects/${projectKey}/refs`)
|
||||||
lastDataSource = 'backend'
|
lastDataSource = 'backend'
|
||||||
@@ -786,6 +796,8 @@ function toProjectConfigs(backendProjects: BackendProject[]): ProjectConfig[] {
|
|||||||
serviceType: serviceTypeLabel(project.key),
|
serviceType: serviceTypeLabel(project.key),
|
||||||
defaultBranch: project.defaultBranch,
|
defaultBranch: project.defaultBranch,
|
||||||
tagPolicy: tagPolicyLabel(project.environments),
|
tagPolicy: tagPolicyLabel(project.environments),
|
||||||
|
giteaStatus: 'warning',
|
||||||
|
giteaMessage: '尚未执行 Gitea 诊断',
|
||||||
environments: project.environments.map((environment) => {
|
environments: project.environments.map((environment) => {
|
||||||
return {
|
return {
|
||||||
name: environment.name,
|
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(
|
function sanitizeProjectJenkinsDiagnostics(
|
||||||
value: ProjectJenkinsDiagnosticsSummary,
|
value: ProjectJenkinsDiagnosticsSummary,
|
||||||
): ProjectJenkinsDiagnosticsSummary {
|
): ProjectJenkinsDiagnosticsSummary {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
IntegrationStatus,
|
IntegrationStatus,
|
||||||
IntegrationConfigStatus,
|
IntegrationConfigStatus,
|
||||||
ProjectConfig,
|
ProjectConfig,
|
||||||
|
ProjectGiteaDiagnosticsSummary,
|
||||||
ProjectJenkinsDiagnosticsSummary,
|
ProjectJenkinsDiagnosticsSummary,
|
||||||
ProjectRefs,
|
ProjectRefs,
|
||||||
JenkinsSyncSummary,
|
JenkinsSyncSummary,
|
||||||
@@ -34,6 +35,7 @@ export const usePlatformStore = defineStore('platform', () => {
|
|||||||
const auditLogs = ref<AuditLogEntry[]>([])
|
const auditLogs = ref<AuditLogEntry[]>([])
|
||||||
const notificationOutbox = ref<NotificationOutboxMessage[]>([])
|
const notificationOutbox = ref<NotificationOutboxMessage[]>([])
|
||||||
const projectJenkinsDiagnostics = ref<ProjectJenkinsDiagnosticsSummary | null>(null)
|
const projectJenkinsDiagnostics = ref<ProjectJenkinsDiagnosticsSummary | null>(null)
|
||||||
|
const projectGiteaDiagnostics = ref<ProjectGiteaDiagnosticsSummary | null>(null)
|
||||||
const integrationConfig = ref<IntegrationConfigStatus>({
|
const integrationConfig = ref<IntegrationConfigStatus>({
|
||||||
checkedAt: '',
|
checkedAt: '',
|
||||||
integrations: [],
|
integrations: [],
|
||||||
@@ -75,6 +77,7 @@ export const usePlatformStore = defineStore('platform', () => {
|
|||||||
releaseProcessXml.value = processXml
|
releaseProcessXml.value = processXml
|
||||||
auditLogs.value = recentAuditLogs
|
auditLogs.value = recentAuditLogs
|
||||||
notificationOutbox.value = recentNotificationOutbox
|
notificationOutbox.value = recentNotificationOutbox
|
||||||
|
applyProjectGiteaDiagnostics()
|
||||||
applyProjectJenkinsDiagnostics()
|
applyProjectJenkinsDiagnostics()
|
||||||
dataSource.value = devopsApi.getLastDataSource()
|
dataSource.value = devopsApi.getLastDataSource()
|
||||||
} finally {
|
} finally {
|
||||||
@@ -135,6 +138,14 @@ export const usePlatformStore = defineStore('platform', () => {
|
|||||||
return diagnostics
|
return diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadProjectGiteaDiagnostics(): Promise<ProjectGiteaDiagnosticsSummary> {
|
||||||
|
const diagnostics = await devopsApi.getProjectGiteaDiagnostics()
|
||||||
|
projectGiteaDiagnostics.value = diagnostics
|
||||||
|
applyProjectGiteaDiagnostics()
|
||||||
|
dataSource.value = devopsApi.getLastDataSource()
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
function applyProjectJenkinsDiagnostics() {
|
function applyProjectJenkinsDiagnostics() {
|
||||||
const diagnostics = projectJenkinsDiagnostics.value
|
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() {
|
async function loadAuditLogs() {
|
||||||
auditLogs.value = await devopsApi.getAuditLogs()
|
auditLogs.value = await devopsApi.getAuditLogs()
|
||||||
dataSource.value = devopsApi.getLastDataSource()
|
dataSource.value = devopsApi.getLastDataSource()
|
||||||
@@ -257,6 +305,7 @@ export const usePlatformStore = defineStore('platform', () => {
|
|||||||
auditLogs,
|
auditLogs,
|
||||||
notificationOutbox,
|
notificationOutbox,
|
||||||
projectJenkinsDiagnostics,
|
projectJenkinsDiagnostics,
|
||||||
|
projectGiteaDiagnostics,
|
||||||
integrationConfig,
|
integrationConfig,
|
||||||
releaseProcessXml,
|
releaseProcessXml,
|
||||||
projectRefs,
|
projectRefs,
|
||||||
@@ -270,6 +319,7 @@ export const usePlatformStore = defineStore('platform', () => {
|
|||||||
syncJenkinsRuns,
|
syncJenkinsRuns,
|
||||||
loadProjectRefs,
|
loadProjectRefs,
|
||||||
loadProjectJenkinsDiagnostics,
|
loadProjectJenkinsDiagnostics,
|
||||||
|
loadProjectGiteaDiagnostics,
|
||||||
loadAuditLogs,
|
loadAuditLogs,
|
||||||
loadNotificationOutbox,
|
loadNotificationOutbox,
|
||||||
retryNotificationOutboxMessage,
|
retryNotificationOutboxMessage,
|
||||||
@@ -308,6 +358,38 @@ function jenkinsDiagnosticMessage(
|
|||||||
return 'Jenkins job 不可用'
|
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(
|
function projectEnvironmentKey(
|
||||||
projectKey: string,
|
projectKey: string,
|
||||||
environment: ReleaseRun['environment'],
|
environment: ReleaseRun['environment'],
|
||||||
|
|||||||
@@ -208,6 +208,13 @@ export interface ProjectConfig {
|
|||||||
serviceType: string
|
serviceType: string
|
||||||
defaultBranch: string
|
defaultBranch: string
|
||||||
tagPolicy: string
|
tagPolicy: string
|
||||||
|
giteaStatus: HealthState
|
||||||
|
giteaMessage?: string
|
||||||
|
giteaWebUrl?: string
|
||||||
|
giteaDefaultBranch?: string
|
||||||
|
giteaBranchCount?: number
|
||||||
|
giteaTagCount?: number
|
||||||
|
giteaLatestCommit?: ProjectRefCommit
|
||||||
environments: EnvironmentStatus[]
|
environments: EnvironmentStatus[]
|
||||||
integrations: {
|
integrations: {
|
||||||
jenkins: HealthState
|
jenkins: HealthState
|
||||||
@@ -251,6 +258,30 @@ export interface ProjectRefCommit {
|
|||||||
authoredAt?: string
|
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 {
|
export interface ProjectRefPullRequest {
|
||||||
number: number
|
number: number
|
||||||
title: string
|
title: string
|
||||||
|
|||||||
@@ -1,24 +1,27 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* 项目配置页展示项目仓库、发布环境以及真实 Jenkins job 诊断状态。
|
* 项目配置页展示项目仓库、发布环境、Gitea 仓库诊断和真实 Jenkins job 诊断状态。
|
||||||
*/
|
*/
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import StatusPill from '../components/StatusPill.vue'
|
import StatusPill from '../components/StatusPill.vue'
|
||||||
import { usePlatformStore } from '../stores/platform'
|
import { usePlatformStore } from '../stores/platform'
|
||||||
import type { EnvironmentStatus } from '../types/devops'
|
import type { EnvironmentStatus, ProjectConfig } from '../types/devops'
|
||||||
|
|
||||||
const platform = usePlatformStore()
|
const platform = usePlatformStore()
|
||||||
const diagnosticsLoading = ref(false)
|
const diagnosticsLoading = ref(false)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void refreshJenkinsDiagnostics()
|
void refreshProjectDiagnostics()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function refreshJenkinsDiagnostics() {
|
async function refreshProjectDiagnostics() {
|
||||||
diagnosticsLoading.value = true
|
diagnosticsLoading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await platform.loadProjectJenkinsDiagnostics()
|
await Promise.all([
|
||||||
|
platform.loadProjectJenkinsDiagnostics(),
|
||||||
|
platform.loadProjectGiteaDiagnostics(),
|
||||||
|
])
|
||||||
} finally {
|
} finally {
|
||||||
diagnosticsLoading.value = false
|
diagnosticsLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -35,6 +38,28 @@ function lastBuildText(env: EnvironmentStatus): string {
|
|||||||
|
|
||||||
return `#${env.jenkinsLastBuild.number} ${env.jenkinsLastBuild.result || 'UNKNOWN'}`
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -54,9 +79,9 @@ function lastBuildText(env: EnvironmentStatus): string {
|
|||||||
size="small"
|
size="small"
|
||||||
plain
|
plain
|
||||||
:loading="diagnosticsLoading"
|
:loading="diagnosticsLoading"
|
||||||
@click="refreshJenkinsDiagnostics"
|
@click="refreshProjectDiagnostics"
|
||||||
>
|
>
|
||||||
刷新 Jenkins 诊断
|
刷新项目诊断
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@@ -64,6 +89,37 @@ function lastBuildText(env: EnvironmentStatus): string {
|
|||||||
<ElTableColumn type="expand">
|
<ElTableColumn type="expand">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="env-config">
|
<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 v-for="env in row.environments" :key="env.name" class="env-config-card">
|
||||||
<div class="env-config-head">
|
<div class="env-config-head">
|
||||||
<strong>{{ env.label }}</strong>
|
<strong>{{ env.label }}</strong>
|
||||||
@@ -157,6 +213,14 @@ function lastBuildText(env: EnvironmentStatus): string {
|
|||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-diagnostic-card {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid #d7e5ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.env-config-head {
|
.env-config-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user