2
0
Files
gitcaddy-server/models/ai/error_pattern.go
logikonline 12f4ea03a8
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Trigger Vault Plugin Rebuild / Trigger Vault Rebuild (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m48s
Build and Release / Lint (push) Failing after 5m2s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux, linux-latest) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 5m37s
refactor: add /v3 suffix to module path for proper Go semver
Go's semantic import versioning requires v2+ modules to include the
major version in the module path. This enables using proper version
tags (v3.x.x) instead of pseudo-versions.

Updated module path: code.gitcaddy.com/server/v3
2026-01-17 17:53:59 -05:00

194 lines
5.8 KiB
Go

// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package ai
import (
"context"
"code.gitcaddy.com/server/v3/models/db"
"code.gitcaddy.com/server/v3/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
}