Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@

# Platform Infrasec
/actions/create-github-app-token @grafana/platform-infrasec

# Mimir
actions/go-flaky-tests/cmd/go-flaky-tests @grafana/mimir-maintainers
4 changes: 4 additions & 0 deletions actions/go-flaky-tests/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@
- Initial implementation of flaky test analysis action
- Loki integration for fetching test failure logs
- Git history analysis to find test authors
- GitHub issue creation and management for flaky tests
- Dry run mode for testing without creating issues
- Comprehensive test suite with golden file testing

### Features

- **Loki Log Analysis**: Fetches and parses test failure logs using LogQL
- **Flaky Test Detection**: Identifies tests that fail inconsistently across branches
- **Git Author Tracking**: Finds recent commits that modified flaky tests
- **GitHub Integration**: Creates and updates issues with detailed test information
- **Configurable Limits**: Top-K filtering to focus on most problematic tests
- **Rich Issue Templates**: Detailed issue descriptions with investigation guidance
26 changes: 25 additions & 1 deletion actions/go-flaky-tests/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# Go Flaky Tests

A GitHub Action that detects and analyzes flaky Go tests by fetching logs from Loki and finding their authors.
A GitHub Action that detects and analyzes flaky Go tests by fetching logs from Loki, finding their authors, and creating GitHub issues to track them.

## Features

- **Loki Integration**: Fetches test failure logs from Loki using LogQL queries
- **Flaky Test Detection**: Identifies tests that fail inconsistently across different branches
- **Git History Analysis**: Finds test files and extracts recent commit authors
- **GitHub Issue Management**: Creates and updates GitHub issues for flaky tests
- **Dry Run Mode**: Preview functionality without creating actual issues

## Usage

Expand All @@ -31,6 +33,7 @@ jobs:
loki-password: ${{ secrets.LOKI_PASSWORD }}
repository: ${{ github.repository }}
time-range: "7d"
skip-posting-issues: "false"
top-k: "5"
```

Expand All @@ -44,6 +47,8 @@ jobs:
| `repository` | Repository name in 'owner/repo' format | ✅ | - |
| `time-range` | Time range for the query (e.g., '1h', '24h', '7d') | ❌ | `1h` |
| `repository-directory` | Relative path to the directory with a git repository | ❌ | `${{ github.workspace }}` |
| `github-token` | GitHub token for repository access | ❌ | `${{ github.token }}` |
| `skip-posting-issues` | Skip creating/updating GitHub issues (dry-run mode) | ❌ | `true` |
| `top-k` | Include only the top K flaky tests by distinct branches count | ❌ | `3` |

## Outputs
Expand All @@ -61,6 +66,8 @@ jobs:
3. **Detect Flaky Tests**: Identifies tests that fail on multiple branches or multiple times on main/master
4. **Find Test Files**: Locates test files in the repository using grep
5. **Extract Authors**: Uses `git log -L` to find recent commits that modified each test
6. **Resolve Usernames**: Looks up GitHub usernames for commit hashes
7. **Create Issues**: Creates or updates GitHub issues with flaky test information

## Flaky Test Detection Logic

Expand All @@ -81,15 +88,32 @@ export LOKI_URL="your-loki-url"
export REPOSITORY="owner/repo"
export TIME_RANGE="24h"
export REPOSITORY_DIRECTORY="."
export SKIP_POSTING_ISSUES="true"

# Run the analysis
./run-local.sh
```

## GitHub Issue Format

The action creates GitHub issues with:

- **Title**: `Flaky test: TestName`
- **Labels**: `flaky-test`
- **Body**: Detailed information about the test including:
- File path and test name
- Investigation tips and next steps
- Recent failure count and affected branches
- Recent authors who modified the test
- Links to failed workflow runs

For existing issues, the action adds comments with updated failure information.

## Requirements

- Go 1.22 or later
- Git repository with test files
- GitHub CLI (automatically available in GitHub Actions)
- Access to Loki instance with test failure logs

## Output Format
Expand Down
17 changes: 17 additions & 0 deletions actions/go-flaky-tests/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ inputs:
description: "Relative path to the directory with a git repository"
required: false
default: ${{ github.workspace }}
github-token:
description: "GitHub token for repository access"
required: false
default: ${{ github.token }}
skip-posting-issues:
description: "Skip creating/updating GitHub issues (dry-run mode)"
required: false
default: "true"
top-k:
description: "Include only the top K flaky tests by distinct branches count in analysis"
required: false
Expand All @@ -36,6 +44,13 @@ runs:
with:
go-version: "1.25"

- name: Setup GitHub CLI
run: |
# GitHub CLI is pre-installed on GitHub Actions runners
# Just verify it's available and authenticated
gh --version
shell: bash

- name: Build and run analyzer
shell: bash
run: |
Expand All @@ -48,5 +63,7 @@ runs:
LOKI_PASSWORD: ${{ inputs.loki-password }}
REPOSITORY: ${{ inputs.repository }}
TIME_RANGE: ${{ inputs.time-range }}
GITHUB_TOKEN: ${{ inputs.github-token }}
REPOSITORY_DIRECTORY: ${{ inputs.repository-directory }}
SKIP_POSTING_ISSUES: ${{ inputs.skip-posting-issues }}
TOP_K: ${{ inputs.top-k }}
Binary file added actions/go-flaky-tests/aggregate
Binary file not shown.
115 changes: 101 additions & 14 deletions actions/go-flaky-tests/cmd/go-flaky-tests/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,19 @@ type GitClient interface {
TestCommits(filePath, testName string) ([]CommitInfo, error)
}

type GitHubClient interface {
GetUsernameForCommit(commitHash string) (string, error)
CreateOrUpdateIssue(test FlakyTest) error
SearchForExistingIssue(issueTitle string) (string, error)
AddCommentToIssue(issueURL string, test FlakyTest) error
ReopenIssue(issueURL string) error
}

type TestFailureAnalyzer struct {
lokiClient LokiClient
gitClient GitClient
fileSystem FileSystem
lokiClient LokiClient
gitClient GitClient
githubClient GitHubClient
fileSystem FileSystem
}

type CommitInfo struct {
Expand Down Expand Up @@ -74,20 +83,22 @@ func (fs *DefaultFileSystem) WriteFile(filename string, data []byte, perm os.Fil
return os.WriteFile(filename, data, perm)
}

func NewTestFailureAnalyzer(loki LokiClient, git GitClient, fs FileSystem) *TestFailureAnalyzer {
func NewTestFailureAnalyzer(loki LokiClient, git GitClient, github GitHubClient, fs FileSystem) *TestFailureAnalyzer {
return &TestFailureAnalyzer{
lokiClient: loki,
gitClient: git,
fileSystem: fs,
lokiClient: loki,
gitClient: git,
githubClient: github,
fileSystem: fs,
}
}

func NewDefaultTestFailureAnalyzer(config Config) *TestFailureAnalyzer {
lokiClient := NewDefaultLokiClient(config)
gitClient := NewDefaultGitClient(config)
githubClient := NewDefaultGitHubClient(config)
fileSystem := &DefaultFileSystem{}

return NewTestFailureAnalyzer(lokiClient, gitClient, fileSystem)
return NewTestFailureAnalyzer(lokiClient, gitClient, githubClient, fileSystem)
}

func (t *TestFailureAnalyzer) AnalyzeFailures(config Config) (*FailuresReport, error) {
Expand Down Expand Up @@ -165,8 +176,26 @@ func (t *TestFailureAnalyzer) AnalyzeFailures(config Config) (*FailuresReport, e
}

func (t *TestFailureAnalyzer) ActionReport(report *FailuresReport, config Config) error {
log.Printf("📝 Report generated successfully - no additional actions in this version")
log.Printf("✅ Analysis complete!")
if report == nil || len(report.FlakyTests) == 0 {
log.Printf("📝 No flaky tests to enact - skipping GitHub issue creation")
return nil
}

if config.SkipPostingIssues {
log.Printf("🔍 Dry run mode: Generating issue previews...")
err := t.previewIssuesForFlakyTests(report.FlakyTests, config)
if err != nil {
return fmt.Errorf("failed to preview GitHub issues: %w", err)
}
} else {
log.Printf("📝 Creating GitHub issues for flaky tests...")
err := t.createIssuesForFlakyTests(report.FlakyTests)
if err != nil {
return fmt.Errorf("failed to create GitHub issues: %w", err)
}
}

log.Printf("✅ Report enactment complete!")
return nil
}

Expand Down Expand Up @@ -222,16 +251,74 @@ func (t *TestFailureAnalyzer) findTestAuthors(flakyTests []FlakyTest) error {
}
flakyTests[i].RecentCommits = commits

if len(commits) > 0 {
var authors []string
for _, commit := range commits {
authors = append(authors, commit.Author)
var authors []string
for commitIdx, commit := range commits {
authors = append(authors, commit.Author)
commits[commitIdx].Author, err = t.githubClient.GetUsernameForCommit(commit.Hash)
if err != nil {
return fmt.Errorf("failed to get author for test %s in %s: %w", test.TestName, test.FilePath, err)
}
}
}
return nil
}

func (t *TestFailureAnalyzer) createIssuesForFlakyTests(flakyTests []FlakyTest) error {
for _, test := range flakyTests {
err := t.githubClient.CreateOrUpdateIssue(test)
if err != nil {
log.Printf("Warning: failed to create issue for test %s: %v", test.TestName, err)
}
}
return nil
}

func (t *TestFailureAnalyzer) previewIssuesForFlakyTests(flakyTests []FlakyTest, config Config) error {
for _, test := range flakyTests {
err := previewIssueForTest(test, config)
if err != nil {
log.Printf("Warning: failed to preview issue for test %s: %v", test.TestName, err)
}
}
return nil
}

func previewIssueForTest(test FlakyTest, config Config) error {
issueTitle := fmt.Sprintf("Flaky %s", test.TestName)

log.Printf("📄 Would create issue for %s:", test.TestName)
log.Printf("Title: %s", issueTitle)
log.Printf("Labels: flaky-test")
log.Printf("")

// Generate the actual markdown content that would be used
issueBody, err := generateInitialIssueBody(test)
if err != nil {
log.Printf("Warning: failed to generate issue body preview: %v", err)
return nil
}

commentBody, err := generateCommentBody(test, config)
if err != nil {
log.Printf("Warning: failed to generate comment body preview: %v", err)
return nil
}

log.Printf("Initial Issue Body Markdown:")
log.Printf("────────────────────────────────────────────────────────────────────────")
log.Printf("%s", issueBody)
log.Printf("────────────────────────────────────────────────────────────────────────")
log.Printf("")

log.Printf("Comment Body Markdown:")
log.Printf("────────────────────────────────────────────────────────────────────────")
log.Printf("%s", commentBody)
log.Printf("────────────────────────────────────────────────────────────────────────")
log.Printf("")

return nil
}

func generateSummary(flakyTests []FlakyTest) string {
if len(flakyTests) == 0 {
return "No flaky tests found in the specified time range."
Expand Down
9 changes: 9 additions & 0 deletions actions/go-flaky-tests/cmd/go-flaky-tests/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type Config struct {
Repository string
TimeRange string
RepositoryDirectory string
SkipPostingIssues bool
TopK int
}

Expand All @@ -23,6 +24,7 @@ func getConfigFromEnv() Config {
Repository: os.Getenv("REPOSITORY"),
TimeRange: getEnvWithDefault("TIME_RANGE", "24h"),
RepositoryDirectory: getEnvWithDefault("REPOSITORY_DIRECTORY", "."),
SkipPostingIssues: getBoolEnvWithDefault("SKIP_POSTING_ISSUES", true),
TopK: getIntEnvWithDefault("TOP_K", 3),
}
}
Expand All @@ -34,6 +36,13 @@ func getEnvWithDefault(key, defaultValue string) string {
return defaultValue
}

func getBoolEnvWithDefault(key string, defaultValue bool) bool {
if value := os.Getenv(key); value != "" {
return value == "true" || value == "1"
}
return defaultValue
}

func getIntEnvWithDefault(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if intValue, err := strconv.Atoi(value); err == nil {
Expand Down
5 changes: 3 additions & 2 deletions actions/go-flaky-tests/cmd/go-flaky-tests/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@ func findTestFilePath(repoDir, testName string) (string, error) {
}

func getFileAuthors(config Config, filePath, testName string) ([]CommitInfo, error) {
return getFileAuthorsWithClient(config.RepositoryDirectory, filePath, testName)
githubClient := NewDefaultGitHubClient(config)
return getFileAuthorsWithClient(config.RepositoryDirectory, filePath, testName, githubClient)
}

func getFileAuthorsWithClient(repoDir, filePath, testName string) ([]CommitInfo, error) {
func getFileAuthorsWithClient(repoDir, filePath, testName string, githubClient GitHubClient) ([]CommitInfo, error) {
// Get 10 commits, because some of them might just be only bots.
cmd := exec.Command("git", "log", "-10", "-L", fmt.Sprintf(":%s:%s", testName, filePath), "--pretty=format:%H|%ct|%s|%an", "-s")
cmd.Dir = repoDir
Expand Down
Loading
Loading