gitlink-cli/shortcuts/workflow/rules/ci_diagnosis.go

176 lines
4.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rules
import (
"fmt"
"regexp"
"github.com/gitlink-org/gitlink-cli/shortcuts/workflow"
)
// CIDiagnosisRule matches CI build error logs against known patterns.
func CIDiagnosisRule(upstream map[string]interface{}, stepName string) (*workflow.AIResponse, error) {
builds := extractList(upstream, "ci-builds")
commits := extractCommits(upstream)
type diagnosis struct {
BuildNumber interface{} `json:"build_number"`
Status string `json:"status"`
Pattern string `json:"matched_pattern"`
Diagnosis string `json:"diagnosis"`
Suggestion string `json:"suggestion"`
RelatedSHA string `json:"related_commit"`
}
var diagnoses []diagnosis
var actions []workflow.AIAction
for _, build := range builds {
status := str(build, "status", "state", "result")
if status != "failed" && status != "failure" && status != "error" && status != "3" {
// Check nested: some APIs use "build" wrapper.
if inner, ok := build["build"].(map[string]interface{}); ok {
build = inner
status = str(build, "status", "state", "result")
if status != "failed" && status != "failure" && status != "error" && status != "3" {
continue
}
} else {
continue
}
}
log := str(build, "log", "logs", "output", "build_log")
if log == "" {
continue
}
d := diagnoseLog(log)
buildNum := build["build_number"]
if buildNum == nil {
buildNum = build["id"]
}
// Find related commit.
relatedSHA := ""
for _, c := range commits {
cSha := str(c, "sha", "id", "commit_id")
if cSha != "" && containsAny(str(c, "title", "message", "commit"), d.Pattern) {
relatedSHA = cSha
break
}
}
diagnoses = append(diagnoses, diagnosis{
BuildNumber: buildNum,
Status: status,
Pattern: d.Pattern,
Diagnosis: d.Diagnosis,
Suggestion: d.Suggestion,
RelatedSHA: relatedSHA,
})
// Auto-retry for transient failures.
if d.Transient {
actions = append(actions, workflow.AIAction{
Type: "api",
Method: "POST",
Path: fmt.Sprintf("{v1}/builds/%v/retry", buildNum),
Body: map[string]interface{}{},
})
}
}
if len(diagnoses) == 0 {
return &workflow.AIResponse{
Analysis: map[string]interface{}{"diagnoses": nil, "message": "no failed builds found"},
Actions: nil,
}, nil
}
analysis := map[string]interface{}{
"diagnoses": diagnoses,
"total_failures": len(diagnoses),
}
return &workflow.AIResponse{Analysis: analysis, Actions: actions}, nil
}
type logPattern struct {
Re *regexp.Regexp
Pattern string
Diagnosis string
Suggestion string
Transient bool
}
var ciPatterns = []logPattern{
{regexp.MustCompile(`cannot find package|package .* is not in`), "cannot find package",
"依赖缺失",
"检查 go.mod/package.json 确认依赖已声明",
false},
{regexp.MustCompile(`syntax error|unexpected token|unexpected EOF`), "syntax error",
"语法错误",
"检查最近提交中的语法问题",
false},
{regexp.MustCompile(`permission denied|access denied|forbidden|401|403`), "permission denied",
"权限不足",
"检查密钥配置和访问权限",
false},
{regexp.MustCompile(`connection refused|connection reset|no route to host|dial tcp`), "connection refused",
"服务不可达",
"检查外部服务状态和网络连接",
true},
{regexp.MustCompile(`out of memory|OOM|killed|signal: killed`), "out of memory",
"资源不足(内存溢出)",
"优化内存使用或增加构建资源",
false},
{regexp.MustCompile(`No such file|file not found|not found`), "No such file",
"文件缺失",
"检查 .devops/ 路径和依赖文件配置",
false},
{regexp.MustCompile(`docker:.*not found|docker.*command not found`), "docker not found",
"Docker 环境缺失",
"构建环境未配置 Docker检查 CI 配置",
false},
{regexp.MustCompile(`FAIL|exit status [1-9]|Test.*failed`), "exit status 1",
"测试失败",
"查看测试输出定位失败用例",
false},
{regexp.MustCompile(`timeout|timed out|deadline exceeded`), "timeout",
"构建超时",
"优化构建脚本或增加超时时间",
true},
{regexp.MustCompile(`undefined:|undefined symbol|cannot use|type mismatch`), "undefined:",
"编译错误(未定义符号)",
"检查导入和类型定义",
false},
}
func diagnoseLog(log string) logPattern {
for _, p := range ciPatterns {
if p.Re.MatchString(log) {
return p
}
}
return logPattern{
Pattern: "unknown",
Diagnosis: "未知错误",
Suggestion: "请人工查看 CI 日志进行诊断",
Transient: false,
}
}
func containsAny(s string, patterns ...string) bool {
for _, p := range patterns {
if p != "" && len(s) > 0 && len(p) > 0 {
// Simple substring check.
if len(s) >= len(p) {
for i := 0; i <= len(s)-len(p); i++ {
if s[i:i+len(p)] == p {
return true
}
}
}
}
}
return false
}