All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m7s
Build and Release / Lint (push) Successful in 5m21s
Build and Release / Unit Tests (push) Successful in 5m46s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m44s
Build and Release / Build Binaries (amd64, darwin, linux-latest) (push) Successful in 4m4s
Build and Release / Build Binaries (arm64, darwin, linux-latest) (push) Successful in 3m23s
Build and Release / Build Binaries (arm64, linux, linux-latest) (push) Successful in 3m47s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 8h6m28s
194 lines
5.8 KiB
Go
194 lines
5.8 KiB
Go
// Copyright 2026 MarketAlly. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package ai
|
|
|
|
import (
|
|
"context"
|
|
|
|
"code.gitcaddy.com/server/models/db"
|
|
"code.gitcaddy.com/server/modules/timeutil"
|
|
)
|
|
|
|
func init() {
|
|
db.RegisterModel(new(ErrorPattern))
|
|
db.RegisterModel(new(WorkflowTelemetry))
|
|
}
|
|
|
|
// ErrorPattern represents a known CI/CD error pattern with its solution
|
|
type ErrorPattern struct {
|
|
ID int64 `xorm:"pk autoincr"`
|
|
Pattern string `xorm:"VARCHAR(255) NOT NULL INDEX"`
|
|
PatternRegex string `xorm:"TEXT"`
|
|
RunnerType string `xorm:"VARCHAR(50) INDEX"`
|
|
ProjectType string `xorm:"VARCHAR(100) INDEX"`
|
|
Framework string `xorm:"VARCHAR(100)"`
|
|
ErrorMessage string `xorm:"TEXT"`
|
|
Diagnosis string `xorm:"TEXT"`
|
|
Solution string `xorm:"TEXT"`
|
|
SolutionDiff string `xorm:"TEXT"`
|
|
OccurrenceCount int `xorm:"DEFAULT 1"`
|
|
SuccessCount int `xorm:"DEFAULT 0"`
|
|
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
|
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
|
}
|
|
|
|
// TableName returns the table name for ErrorPattern
|
|
func (ErrorPattern) TableName() string {
|
|
return "error_pattern"
|
|
}
|
|
|
|
// WorkflowTelemetry records workflow run results for compatibility tracking
|
|
type WorkflowTelemetry struct {
|
|
ID int64 `xorm:"pk autoincr"`
|
|
RunnerID int64 `xorm:"INDEX"`
|
|
RunnerName string `xorm:"VARCHAR(255) INDEX"`
|
|
JobID int64 `xorm:"INDEX"`
|
|
WorkflowName string `xorm:"VARCHAR(255)"`
|
|
ProjectType string `xorm:"VARCHAR(100) INDEX"`
|
|
Framework string `xorm:"VARCHAR(100)"`
|
|
Target string `xorm:"VARCHAR(100)"`
|
|
Status string `xorm:"VARCHAR(20) INDEX"`
|
|
ErrorPatternID int64 `xorm:"INDEX"`
|
|
DurationSeconds int
|
|
CreatedUnix timeutil.TimeStamp `xorm:"created INDEX"`
|
|
}
|
|
|
|
// TableName returns the table name for WorkflowTelemetry
|
|
func (WorkflowTelemetry) TableName() string {
|
|
return "workflow_telemetry"
|
|
}
|
|
|
|
// GetErrorPatterns returns matching error patterns
|
|
func GetErrorPatterns(ctx context.Context, pattern, runnerType, projectType string) ([]*ErrorPattern, error) {
|
|
patterns := make([]*ErrorPattern, 0, 10)
|
|
sess := db.GetEngine(ctx).OrderBy("occurrence_count DESC")
|
|
if pattern != "" {
|
|
sess = sess.Where("pattern LIKE ?", "%"+pattern+"%")
|
|
}
|
|
// Handle special "_any" filter for patterns with empty runner_type
|
|
if runnerType == "_any" {
|
|
sess = sess.And("runner_type = ?", "")
|
|
} else if runnerType != "" {
|
|
sess = sess.And("runner_type = ?", runnerType)
|
|
}
|
|
if projectType != "" {
|
|
sess = sess.And("project_type = ?", projectType)
|
|
}
|
|
return patterns, sess.Limit(50).Find(&patterns)
|
|
}
|
|
|
|
// GetErrorPatternByID returns an error pattern by ID
|
|
func GetErrorPatternByID(ctx context.Context, id int64) (*ErrorPattern, error) {
|
|
ep := &ErrorPattern{}
|
|
has, err := db.GetEngine(ctx).ID(id).Get(ep)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !has {
|
|
return nil, nil
|
|
}
|
|
return ep, nil
|
|
}
|
|
|
|
// CreateOrUpdateErrorPattern creates or updates an error pattern
|
|
func CreateOrUpdateErrorPattern(ctx context.Context, ep *ErrorPattern) error {
|
|
existing := &ErrorPattern{}
|
|
has, err := db.GetEngine(ctx).
|
|
Where("pattern = ? AND runner_type = ? AND project_type = ?",
|
|
ep.Pattern, ep.RunnerType, ep.ProjectType).
|
|
Get(existing)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if has {
|
|
existing.OccurrenceCount++
|
|
if ep.Solution != "" {
|
|
existing.Solution = ep.Solution
|
|
existing.SolutionDiff = ep.SolutionDiff
|
|
existing.Diagnosis = ep.Diagnosis
|
|
}
|
|
if ep.ErrorMessage != "" && existing.ErrorMessage == "" {
|
|
existing.ErrorMessage = ep.ErrorMessage
|
|
}
|
|
_, err = db.GetEngine(ctx).ID(existing.ID).AllCols().Update(existing)
|
|
return err
|
|
}
|
|
_, err = db.GetEngine(ctx).Insert(ep)
|
|
return err
|
|
}
|
|
|
|
// IncrementSuccessCount marks a solution as successful
|
|
func IncrementSuccessCount(ctx context.Context, id int64) error {
|
|
_, err := db.GetEngine(ctx).Exec(
|
|
"UPDATE error_pattern SET success_count = success_count + 1, updated_unix = ? WHERE id = ?",
|
|
timeutil.TimeStampNow(), id)
|
|
return err
|
|
}
|
|
|
|
// RecordTelemetry records a workflow run result
|
|
func RecordTelemetry(ctx context.Context, t *WorkflowTelemetry) error {
|
|
_, err := db.GetEngine(ctx).Insert(t)
|
|
return err
|
|
}
|
|
|
|
// GetCompatibilityMatrix returns what works on which runners
|
|
func GetCompatibilityMatrix(ctx context.Context, projectType string) ([]map[string]any, error) {
|
|
results := make([]map[string]any, 0)
|
|
|
|
// Use XORM's native query which returns []map[string][]byte
|
|
sql := "SELECT runner_name, framework, target, " +
|
|
"SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successes, " +
|
|
"SUM(CASE WHEN status = 'failure' THEN 1 ELSE 0 END) as failures, " +
|
|
"COUNT(*) as total " +
|
|
"FROM workflow_telemetry WHERE 1=1"
|
|
if projectType != "" {
|
|
sql += " AND project_type = ?"
|
|
}
|
|
sql += " GROUP BY runner_name, framework, target ORDER BY total DESC"
|
|
|
|
var rows []map[string][]byte
|
|
var err error
|
|
if projectType != "" {
|
|
rows, err = db.GetEngine(ctx).Query(sql, projectType)
|
|
} else {
|
|
rows, err = db.GetEngine(ctx).Query(sql)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, row := range rows {
|
|
successes := parseInt(row["successes"])
|
|
failures := parseInt(row["failures"])
|
|
total := parseInt(row["total"])
|
|
successRate := float64(0)
|
|
if total > 0 {
|
|
successRate = float64(successes) / float64(total) * 100
|
|
}
|
|
results = append(results, map[string]any{
|
|
"runner_name": string(row["runner_name"]),
|
|
"framework": string(row["framework"]),
|
|
"target": string(row["target"]),
|
|
"successes": successes,
|
|
"failures": failures,
|
|
"total": total,
|
|
"success_rate": successRate,
|
|
})
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func parseInt(b []byte) int64 {
|
|
if len(b) == 0 {
|
|
return 0
|
|
}
|
|
var n int64
|
|
for _, c := range b {
|
|
if c >= '0' && c <= '9' {
|
|
n = n*10 + int64(c-'0')
|
|
}
|
|
}
|
|
return n
|
|
}
|