Skip to main content
Glama
check-blocked.ts7.3 kB
/** * Workflow Check Blocked Command */ import chalk from 'chalk'; import * as fs from 'fs'; import * as path from 'path'; import { parseConfig, getLabelPrefixes, matchLabel, buildLabel } from '../../../utils/workflow-config.js'; import { createClient as createClientAsync, getContextFromConfig } from '../../utils/client.js'; export interface CheckBlockedOptions { token?: string; server?: string; owner?: string; repo?: string; threshold?: string; json?: boolean; } interface BlockedIssue { number: number; title: string; priority: string | null; ageHours: number; slaHours: number; exceededBy: number; status: 'blocked' | 'warning' | 'ok'; } /** * 检测阻塞的 Issue */ export async function checkBlocked(options: CheckBlockedOptions): Promise<void> { const configPath = path.join(process.cwd(), '.gitea', 'issue-workflow.yaml'); if (!fs.existsSync(configPath)) { if (options.json) { console.log(JSON.stringify({ error: 'Config file not found' }, null, 2)); } else { console.log(chalk.red(`\n❌ 未找到工作流配置文件: ${configPath}`)); console.log(chalk.yellow('\n💡 提示: 运行 `keactl workflow init` 初始化配置')); } return; } // 读取并解析配置 const configContent = fs.readFileSync(configPath, 'utf-8'); const parseResult = parseConfig(configContent); if (!parseResult.success || !parseResult.config) { if (options.json) { console.log(JSON.stringify({ error: 'Config parse error' }, null, 2)); } else { console.log(chalk.red('\n❌ 配置文件解析失败')); } return; } const config = parseResult.config; // 获取上下文 const context = getContextFromConfig(); const owner = options.owner || context.owner; const repo = options.repo || context.repo; if (!owner || !repo) { if (options.json) { console.log(JSON.stringify({ error: 'Missing owner or repo' }, null, 2)); } else { console.log(chalk.red('\n❌ 请提供仓库信息: --owner 和 --repo')); } return; } if (!options.json) { console.log(chalk.bold(`\n🚨 检测阻塞 Issue - ${owner}/${repo}\n`)); } // 创建客户端 const client = await createClientAsync({ token: options.token, server: options.server, }); if (!client) { if (options.json) { console.log(JSON.stringify({ error: 'Cannot create API client' }, null, 2)); } else { console.log(chalk.red('\n❌ 无法创建 API 客户端,请检查配置')); } return; } // SLA 配置 - 从 priority labels 的 sla_hours 获取 const sla: Record<string, number> = { P0: config.labels.priority['P0']?.sla_hours || 4, P1: config.labels.priority['P1']?.sla_hours || 24, P2: config.labels.priority['P2']?.sla_hours || 72, P3: config.labels.priority['P3']?.sla_hours || 168, }; const thresholdHours = options.threshold ? parseInt(options.threshold) : undefined; try { // 获取开放的 Issues const issues = await client.get<Array<{ number?: number; title?: string; labels?: Array<{ name?: string }>; updated_at?: string; created_at?: string; }>>(`/repos/${owner}/${repo}/issues`, { state: 'open' }); const now = Date.now(); const results: BlockedIssue[] = []; for (const issue of issues) { const labels = (issue.labels || []).map((l) => l.name || ''); const prefixes = getLabelPrefixes(config); // 获取优先级 const priorityLabel = labels.find((l) => matchLabel(prefixes.priority, l) !== null); const priority = priorityLabel ? matchLabel(prefixes.priority, priorityLabel)?.toUpperCase() ?? null : null; // 获取 SLA let issueSla: number; if (thresholdHours !== undefined) { issueSla = thresholdHours; } else if (priority && priority in sla) { issueSla = sla[priority as keyof typeof sla]; } else { issueSla = sla.P3; // 默认使用 P3 的 SLA } // 计算 Issue 年龄(基于最后更新时间) const updatedAt = new Date(issue.updated_at || issue.created_at || now).getTime(); const ageHours = Math.round((now - updatedAt) / (1000 * 60 * 60)); // 检查是否超过 SLA const exceededBy = ageHours - issueSla; let status: 'blocked' | 'warning' | 'ok'; if (exceededBy > 0) { status = 'blocked'; } else if (exceededBy > -issueSla * 0.2) { // 接近 SLA 80% status = 'warning'; } else { status = 'ok'; } results.push({ number: issue.number || 0, title: issue.title || '', priority, ageHours, slaHours: issueSla, exceededBy, status, }); } // 排序:阻塞 > 警告 > 正常,按超时时间排序 results.sort((a, b) => { const statusOrder = { blocked: 0, warning: 1, ok: 2 }; if (statusOrder[a.status] !== statusOrder[b.status]) { return statusOrder[a.status] - statusOrder[b.status]; } return b.exceededBy - a.exceededBy; }); const blocked = results.filter((r) => r.status === 'blocked'); const warning = results.filter((r) => r.status === 'warning'); if (options.json) { console.log(JSON.stringify({ owner, repo, totalIssues: results.length, blockedCount: blocked.length, warningCount: warning.length, sla, issues: results.filter((r) => r.status !== 'ok'), }, null, 2)); return; } // 显示结果 if (blocked.length === 0 && warning.length === 0) { console.log(chalk.green('✓ 没有检测到阻塞或即将超时的 Issue\n')); return; } if (blocked.length > 0) { console.log(chalk.red.bold(`🚨 阻塞 Issue (${blocked.length}):\n`)); for (const issue of blocked) { const priorityStr = issue.priority ? chalk.cyan(`[${issue.priority}]`) : chalk.gray('[无优先级]'); console.log(` #${issue.number} ${issue.title}`); console.log(` ${priorityStr} 超时 ${chalk.red(issue.exceededBy + 'h')} (年龄: ${issue.ageHours}h, SLA: ${issue.slaHours}h)`); console.log(); } } if (warning.length > 0) { console.log(chalk.yellow.bold(`⚠️ 即将超时 (${warning.length}):\n`)); for (const issue of warning) { const priorityStr = issue.priority ? chalk.cyan(`[${issue.priority}]`) : chalk.gray('[无优先级]'); const remaining = issue.slaHours - issue.ageHours; console.log(` #${issue.number} ${issue.title}`); console.log(` ${priorityStr} 剩余 ${chalk.yellow(remaining + 'h')} (年龄: ${issue.ageHours}h, SLA: ${issue.slaHours}h)`); console.log(); } } // 显示 SLA 配置 console.log(chalk.gray('SLA 配置:')); console.log(chalk.gray(` P0 紧急: ${sla.P0}h | P1 高: ${sla.P1}h | P2 中: ${sla.P2}h | P3 低: ${sla.P3}h`)); } catch (error) { if (options.json) { console.log(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2)); } else { console.log(chalk.red(`\n❌ 检测失败: ${error instanceof Error ? error.message : String(error)}`)); } } if (!options.json) { console.log(); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/SupenBysz/gitea-mcp-tool'

If you have feedback or need assistance with the MCP directory API, please join our Discord server