Created
February 6, 2026 14:12
-
-
Save StefMa/5e0f7c4ae6043d22c803e464f437a915 to your computer and use it in GitHub Desktop.
aiir – summarize github actions failures with ollama
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "encoding/json" | |
| "errors" | |
| "fmt" | |
| "os" | |
| "os/exec" | |
| "strconv" | |
| "strings" | |
| ) | |
| type PRCheck struct { | |
| Link string `json:"link"` | |
| Name string `json:"name"` | |
| State string `json:"state"` | |
| Workflow string `json:"workflow"` | |
| } | |
| func main() { | |
| if len(os.Args) != 2 { | |
| _, _ = fmt.Fprintln(os.Stderr, "Usage: ./aiir NUMBER (where NUMBER is a PR number)") | |
| os.Exit(1) | |
| } | |
| prNumber, err := strconv.Atoi(os.Args[1]) | |
| if err != nil { | |
| _, _ = fmt.Fprintf(os.Stderr, "Invalid PR number: %v\n", os.Args[1]) | |
| os.Exit(1) | |
| } | |
| if !isGhAvailable() { | |
| _, _ = fmt.Fprintln(os.Stderr, "Error: 'gh' CLI not found in PATH. Please install GitHub CLI.") | |
| os.Exit(1) | |
| } | |
| checks, err := getPRChecks(prNumber) | |
| if err != nil { | |
| _, _ = fmt.Fprintf(os.Stderr, "Error running 'gh pr checks': %v\n", err) | |
| os.Exit(1) | |
| } | |
| failures := []PRCheck{} | |
| for _, check := range checks { | |
| if strings.ToUpper(check.State) == "FAILURE" { | |
| failures = append(failures, check) | |
| } | |
| } | |
| if len(failures) == 0 { | |
| fmt.Printf("All checks succeeded for PR #%d.\n", prNumber) | |
| os.Exit(0) | |
| } | |
| for _, fail := range failures { | |
| jobID, err := extractJobID(fail.Link) | |
| if err != nil { | |
| _, _ = fmt.Fprintf(os.Stderr, "Could not extract job ID from link: %s\n", fail.Link) | |
| continue | |
| } | |
| fmt.Printf("\n--- Failure: %s (Workflow: %s) ---\n", fail.Name, fail.Workflow) | |
| log, err := getJobLog(jobID) | |
| if err != nil { | |
| _, _ = fmt.Fprintf(os.Stderr, "Error getting log for job %s: %v\n", jobID, err) | |
| continue | |
| } | |
| fmt.Println(log) | |
| ollamaOutput, err := runOllama(log) | |
| if err != nil { | |
| _, _ = fmt.Fprintf(os.Stderr, "Error running ollama: %v\n", err) | |
| continue | |
| } | |
| fmt.Printf("\n--- AI Analysis ---\n%s\n", ollamaOutput) | |
| } | |
| // No os.Exit(1) here; always exit 0 | |
| } | |
| func isGhAvailable() bool { | |
| _, err := exec.LookPath("gh") | |
| return err == nil | |
| } | |
| func getPRChecks(prNumber int) ([]PRCheck, error) { | |
| cmd := exec.Command("gh", "pr", "checks", strconv.Itoa(prNumber), "--json", "workflow,name,link,state") | |
| output, err := cmd.Output() | |
| if err != nil { | |
| return nil, err | |
| } | |
| var checks []PRCheck | |
| if err := json.Unmarshal(output, &checks); err != nil { | |
| return nil, err | |
| } | |
| return checks, nil | |
| } | |
| func extractJobID(link string) (string, error) { | |
| parts := strings.Split(strings.TrimRight(link, "/"), "/") | |
| if len(parts) < 1 { | |
| return "", errors.New("link does not contain enough segments") | |
| } | |
| jobID := parts[len(parts)-1] | |
| if _, err := strconv.ParseInt(jobID, 10, 64); err != nil { | |
| return "", errors.New("last segment is not a valid job ID") | |
| } | |
| return jobID, nil | |
| } | |
| func getJobLog(jobID string) (string, error) { | |
| cmd := exec.Command("gh", "run", "view", "--job", jobID, "--log-failed") | |
| output, err := cmd.CombinedOutput() | |
| if err != nil { | |
| return "", fmt.Errorf("%v: %s", err, string(output)) | |
| } | |
| return string(output), nil | |
| } | |
| // runOllama calls the ollama CLI with the log and a prompt, returns the AI output. | |
| func runOllama(log string) (string, error) { | |
| instructions := ` | |
| Task: Analyze the CI job log above. | |
| Rules: | |
| • Use only information explicitly present in the log. | |
| • Do not infer, guess, or add context beyond the log. | |
| • Ignore all warnings, notices, and informational messages. | |
| • Focus only on errors that cause or contribute to job failure. | |
| Output: | |
| • Identify the primary error(s). | |
| • Point to the exact location if available (file name, path, line number, module, step, or command). | |
| • Briefly explain why the error occurred, strictly based on the log. | |
| If the log does not contain enough information to locate the issue in the code, explicitly say so. | |
| ` | |
| prompt := log + "\n\n/nothink\n\n" + instructions | |
| cmd := exec.Command("ollama", "run", "qwen3:8b") | |
| stdin, err := cmd.StdinPipe() | |
| if err != nil { | |
| return "", err | |
| } | |
| go func() { | |
| // Ignore error from Close for linter cleanliness | |
| defer func() { _ = stdin.Close() }() | |
| _, _ = stdin.Write([]byte(prompt)) | |
| }() | |
| output, err := cmd.CombinedOutput() | |
| if err != nil { | |
| return "", fmt.Errorf("%v: %s", err, string(output)) | |
| } | |
| return string(output), nil | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment