forked from chroe/gitlink-cli
176 lines
4.8 KiB
Go
176 lines
4.8 KiB
Go
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
|
||
}
|