Skip to content

Commit f373285

Browse files
authored
DX-90 Flakeguard: include codeowners test path and failed test log url in splunk (#1671)
1 parent de11cba commit f373285

File tree

10 files changed

+469
-383
lines changed

10 files changed

+469
-383
lines changed

tools/flakeguard/cmd/aggregate_results.go

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ var AggregateResultsCmd = &cobra.Command{
3131
githubWorkflowName, _ := cmd.Flags().GetString("github-workflow-name")
3232
githubWorkflowRunURL, _ := cmd.Flags().GetString("github-workflow-run-url")
3333
reportID, _ := cmd.Flags().GetString("report-id")
34-
splunkURL, _ := cmd.Flags().GetString("splunk-url")
35-
splunkToken, _ := cmd.Flags().GetString("splunk-token")
36-
splunkEvent, _ := cmd.Flags().GetString("splunk-event")
3734

3835
initialDirSize, err := getDirSize(resultsPath)
3936
if err != nil {
@@ -56,8 +53,9 @@ var AggregateResultsCmd = &cobra.Command{
5653
// Load test reports from JSON files and aggregate them
5754
aggregatedReport, err := reports.LoadAndAggregate(
5855
resultsPath,
56+
reports.WithRepoPath(repoPath),
57+
reports.WithCodeOwnersPath(codeOwnersPath),
5958
reports.WithReportID(reportID),
60-
reports.WithSplunk(splunkURL, splunkToken, splunkEvent),
6159
reports.WithBranchName(branchName),
6260
reports.WithBaseSha(baseSHA),
6361
reports.WithHeadSha(headSHA),
@@ -75,36 +73,9 @@ var AggregateResultsCmd = &cobra.Command{
7573

7674
// Start spinner for mapping test results to paths
7775
s = spinner.New(spinner.CharSets[11], 100*time.Millisecond)
78-
s.Suffix = " Mapping test results to paths..."
76+
s.Suffix = " Filter failed tests..."
7977
s.Start()
8078

81-
// Map test results to test paths
82-
err = reports.MapTestResultsToPaths(aggregatedReport, repoPath)
83-
if err != nil {
84-
s.Stop()
85-
log.Error().Stack().Err(err).Msg("Error mapping test results to paths")
86-
os.Exit(ErrorExitCode)
87-
}
88-
s.Stop()
89-
log.Debug().Msg("Successfully mapped paths to test results")
90-
91-
// Map test results to code owners if codeOwnersPath is provided
92-
if codeOwnersPath != "" {
93-
s = spinner.New(spinner.CharSets[11], 100*time.Millisecond)
94-
s.Suffix = " Mapping test results to code owners..."
95-
s.Start()
96-
fmt.Println()
97-
98-
err = reports.MapTestResultsToOwners(aggregatedReport, codeOwnersPath)
99-
if err != nil {
100-
s.Stop()
101-
log.Error().Stack().Err(err).Msg("Error mapping test results to code owners")
102-
os.Exit(ErrorExitCode)
103-
}
104-
s.Stop()
105-
log.Debug().Msg("Successfully mapped code owners to test results")
106-
}
107-
10879
failedTests := reports.FilterTests(aggregatedReport.Results, func(tr reports.TestResult) bool {
10980
return !tr.Skipped && tr.PassRatio < maxPassRatio
11081
})
@@ -190,9 +161,6 @@ func init() {
190161
AggregateResultsCmd.Flags().String("github-workflow-name", "", "GitHub workflow name for the test report")
191162
AggregateResultsCmd.Flags().String("github-workflow-run-url", "", "GitHub workflow run URL for the test report")
192163
AggregateResultsCmd.Flags().String("report-id", "", "Optional identifier for the test report. Will be generated if not provided")
193-
AggregateResultsCmd.Flags().String("splunk-url", "", "Optional url to simultaneously send the test results to splunk")
194-
AggregateResultsCmd.Flags().String("splunk-token", "", "Optional Splunk HEC token to simultaneously send the test results to splunk")
195-
AggregateResultsCmd.Flags().String("splunk-event", "manual", "Optional Splunk event to send as the triggering event for the test results")
196164

197165
if err := AggregateResultsCmd.MarkFlagRequired("results-path"); err != nil {
198166
log.Fatal().Err(err).Msg("Error marking flag as required")

tools/flakeguard/cmd/generate_report.go

Lines changed: 7 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,18 @@
11
package cmd
22

33
import (
4-
"context"
54
"encoding/json"
65
"fmt"
76
"os"
87
"path/filepath"
9-
"strings"
108
"time"
119

1210
"github.com/briandowns/spinner"
13-
"github.com/google/go-github/v67/github"
1411
"github.com/rs/zerolog/log"
1512
"github.com/smartcontractkit/chainlink-testing-framework/tools/flakeguard/reports"
1613
"github.com/spf13/cobra"
17-
"golang.org/x/oauth2"
1814
)
1915

20-
const exampleGitHubToken = "EXAMPLE_GITHUB_TOKEN" //nolint:gosec
21-
2216
var GenerateReportCmd = &cobra.Command{
2317
Use: "generate-report",
2418
Short: "Generate test reports from aggregated results that can be posted to GitHub",
@@ -30,23 +24,16 @@ var GenerateReportCmd = &cobra.Command{
3024
outputDir, _ := cmd.Flags().GetString("output-path")
3125
maxPassRatio, _ := cmd.Flags().GetFloat64("max-pass-ratio")
3226
generatePRComment, _ := cmd.Flags().GetBool("generate-pr-comment")
33-
githubRepo, _ := cmd.Flags().GetString("github-repository")
34-
githubRunID, _ := cmd.Flags().GetInt64("github-run-id")
35-
artifactName, _ := cmd.Flags().GetString("failed-tests-artifact-name")
27+
failedLogsURL, _ := cmd.Flags().GetString("failed-logs-url")
28+
29+
failedLogsArtifactName := "failed-test-results-with-logs.json"
3630

3731
initialDirSize, err := getDirSize(outputDir)
3832
if err != nil {
3933
log.Error().Err(err).Str("path", outputDir).Msg("Error getting initial directory size")
4034
// intentionally don't exit here, as we can still proceed with the generation
4135
}
4236

43-
// Get the GitHub token from environment variable
44-
githubToken := os.Getenv("GITHUB_TOKEN")
45-
if githubToken == "" {
46-
log.Error().Msg("GITHUB_TOKEN environment variable is not set")
47-
os.Exit(ErrorExitCode)
48-
}
49-
5037
// Load the aggregated report
5138
s := spinner.New(spinner.CharSets[11], 100*time.Millisecond)
5239
s.Suffix = " Loading aggregated test report..."
@@ -72,19 +59,6 @@ var GenerateReportCmd = &cobra.Command{
7259
fmt.Println()
7360
log.Info().Msg("Successfully loaded aggregated test report")
7461

75-
// Check if there are failed tests
76-
hasFailedTests := aggregatedReport.SummaryData.FailedRuns > 0
77-
78-
var artifactLink string
79-
if hasFailedTests && githubRepo != "" && githubRunID != 0 && artifactName != "" {
80-
// Fetch artifact link from GitHub API
81-
artifactLink, err = fetchArtifactLinkWithRetry(githubToken, githubRepo, githubRunID, artifactName, 5, 5*time.Second)
82-
if err != nil {
83-
log.Error().Err(err).Msg("Error fetching artifact link")
84-
os.Exit(ErrorExitCode)
85-
}
86-
}
87-
8862
// Create output directory if it doesn't exist
8963
if err := fs.MkdirAll(outputDir, 0755); err != nil {
9064
log.Error().Err(err).Msg("Error creating output directory")
@@ -96,7 +70,7 @@ var GenerateReportCmd = &cobra.Command{
9670
s.Suffix = " Generating GitHub summary markdown..."
9771
s.Start()
9872

99-
err = generateGitHubSummaryMarkdown(aggregatedReport, filepath.Join(outputDir, "all-test"), artifactLink, artifactName)
73+
err = generateGitHubSummaryMarkdown(aggregatedReport, filepath.Join(outputDir, "all-test"), failedLogsURL, failedLogsArtifactName)
10074
if err != nil {
10175
s.Stop()
10276
fmt.Println()
@@ -147,8 +121,8 @@ var GenerateReportCmd = &cobra.Command{
147121
currentCommitSHA,
148122
repoURL,
149123
actionRunID,
150-
artifactName,
151-
artifactLink,
124+
failedLogsArtifactName,
125+
failedLogsURL,
152126
maxPassRatio,
153127
)
154128
if err != nil {
@@ -184,90 +158,14 @@ func init() {
184158
GenerateReportCmd.Flags().String("action-run-id", "", "The GitHub Actions run ID (required if generate-pr-comment is set)")
185159
GenerateReportCmd.Flags().String("github-repository", "", "The GitHub repository in the format owner/repo (required)")
186160
GenerateReportCmd.Flags().Int64("github-run-id", 0, "The GitHub Actions run ID (required)")
187-
GenerateReportCmd.Flags().String("failed-tests-artifact-name", "failed-test-results-with-logs.json", "The name of the failed tests artifact (default 'failed-test-results-with-logs.json')")
161+
GenerateReportCmd.Flags().String("failed-logs-url", "", "Optional URL linking to additional logs for failed tests")
188162

189163
if err := GenerateReportCmd.MarkFlagRequired("aggregated-results-path"); err != nil {
190164
log.Error().Err(err).Msg("Error marking flag as required")
191165
os.Exit(ErrorExitCode)
192166
}
193167
}
194168

195-
func fetchArtifactLink(githubToken, githubRepo string, githubRunID int64, artifactName string) (string, error) {
196-
if githubToken == exampleGitHubToken {
197-
return "https://example-artifact-link.com", nil
198-
}
199-
ctx := context.Background()
200-
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: githubToken})
201-
tc := oauth2.NewClient(ctx, ts)
202-
client := github.NewClient(tc)
203-
204-
// Split owner/repo
205-
repoParts := strings.SplitN(githubRepo, "/", 2)
206-
if len(repoParts) != 2 {
207-
return "", fmt.Errorf("invalid format for --github-repository, expected owner/repo")
208-
}
209-
owner, repo := repoParts[0], repoParts[1]
210-
211-
opts := &github.ListOptions{PerPage: 100} // The max GitHub allows is 100 per page
212-
var allArtifacts []*github.Artifact
213-
214-
// Paginate through all artifacts
215-
for {
216-
artifacts, resp, err := client.Actions.ListWorkflowRunArtifacts(ctx, owner, repo, githubRunID, opts)
217-
if err != nil {
218-
return "", fmt.Errorf("error listing artifacts: %w", err)
219-
}
220-
221-
allArtifacts = append(allArtifacts, artifacts.Artifacts...)
222-
223-
if resp.NextPage == 0 {
224-
// No more pages
225-
break
226-
}
227-
// Move to the next page
228-
opts.Page = resp.NextPage
229-
}
230-
231-
// Find the artifact
232-
for _, artifact := range allArtifacts {
233-
if artifact.GetName() == artifactName {
234-
artifactID := artifact.GetID()
235-
artifactURL := fmt.Sprintf("https://github.com/%s/%s/actions/runs/%d/artifacts/%d",
236-
owner, repo, githubRunID, artifactID)
237-
return artifactURL, nil
238-
}
239-
}
240-
241-
return "", fmt.Errorf("artifact '%s' not found in the workflow run", artifactName)
242-
}
243-
244-
func fetchArtifactLinkWithRetry(
245-
githubToken, githubRepo string,
246-
githubRunID int64, artifactName string,
247-
maxRetries int, delay time.Duration,
248-
) (string, error) {
249-
var lastErr error
250-
for attempt := 1; attempt <= maxRetries; attempt++ {
251-
link, err := fetchArtifactLink(githubToken, githubRepo, githubRunID, artifactName)
252-
if err == nil {
253-
// Found the artifact link successfully
254-
return link, nil
255-
}
256-
257-
// If this was our last attempt, return the error
258-
lastErr = err
259-
if attempt == maxRetries {
260-
break
261-
}
262-
263-
// Otherwise wait and retry
264-
log.Printf("[Attempt %d/%d] Artifact not yet available. Retrying in %s...", attempt, maxRetries, delay)
265-
time.Sleep(delay)
266-
}
267-
268-
return "", fmt.Errorf("failed to fetch artifact link after %d retries: %w", maxRetries, lastErr)
269-
}
270-
271169
func generateGitHubSummaryMarkdown(report *reports.TestReport, outputPath, artifactLink, artifactName string) error {
272170
fs := reports.OSFileSystem{}
273171
mdFileName := outputPath + "-summary.md"
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"strings"
8+
"time"
9+
10+
"github.com/google/go-github/v67/github"
11+
"github.com/rs/zerolog/log"
12+
"github.com/spf13/cobra"
13+
"golang.org/x/oauth2"
14+
)
15+
16+
// GetGHArtifactLinkCmd fetches the artifact link from GitHub API.
17+
var GetGHArtifactLinkCmd = &cobra.Command{
18+
Use: "get-gh-artifact",
19+
Short: "Get artifact link from GitHub API",
20+
Run: func(cmd *cobra.Command, args []string) {
21+
// Get flag values
22+
githubRepo, _ := cmd.Flags().GetString("github-repository")
23+
githubRunID, _ := cmd.Flags().GetInt64("github-run-id")
24+
artifactName, _ := cmd.Flags().GetString("failed-tests-artifact-name")
25+
26+
// Get the GitHub token from environment variable
27+
githubToken := os.Getenv("GITHUB_TOKEN")
28+
if githubToken == "" {
29+
log.Error().Msg("GITHUB_TOKEN environment variable is not set")
30+
os.Exit(ErrorExitCode)
31+
}
32+
33+
// Fetch artifact link from GitHub API with retry logic
34+
artifactLink, err := fetchArtifactLinkWithRetry(githubToken, githubRepo, githubRunID, artifactName, 5, 5*time.Second)
35+
if err != nil {
36+
log.Error().Err(err).Msg("Error fetching artifact link")
37+
os.Exit(ErrorExitCode)
38+
}
39+
40+
fmt.Println(artifactLink)
41+
},
42+
}
43+
44+
func init() {
45+
GetGHArtifactLinkCmd.Flags().String("github-repository", "", "The GitHub repository in the format owner/repo (required)")
46+
GetGHArtifactLinkCmd.Flags().Int64("github-run-id", 0, "The GitHub Actions run ID (required)")
47+
GetGHArtifactLinkCmd.Flags().String("failed-tests-artifact-name", "failed-test-results-with-logs.json", "The name of the failed tests artifact (default 'failed-test-results-with-logs.json')")
48+
49+
if err := GetGHArtifactLinkCmd.MarkFlagRequired("github-repository"); err != nil {
50+
log.Error().Err(err).Msg("Error marking github-repository flag as required")
51+
os.Exit(ErrorExitCode)
52+
}
53+
if err := GetGHArtifactLinkCmd.MarkFlagRequired("github-run-id"); err != nil {
54+
log.Error().Err(err).Msg("Error marking github-run-id flag as required")
55+
os.Exit(ErrorExitCode)
56+
}
57+
}
58+
59+
// fetchArtifactLink uses the GitHub API to retrieve the artifact link.
60+
func fetchArtifactLink(githubToken, githubRepo string, githubRunID int64, artifactName string) (string, error) {
61+
ctx := context.Background()
62+
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: githubToken})
63+
tc := oauth2.NewClient(ctx, ts)
64+
client := github.NewClient(tc)
65+
66+
// Split owner and repo from the provided repository string.
67+
repoParts := strings.SplitN(githubRepo, "/", 2)
68+
if len(repoParts) != 2 {
69+
return "", fmt.Errorf("invalid format for --github-repository, expected owner/repo")
70+
}
71+
owner, repo := repoParts[0], repoParts[1]
72+
73+
opts := &github.ListOptions{PerPage: 100} // maximum per page allowed by GitHub
74+
var allArtifacts []*github.Artifact
75+
76+
// Paginate through all artifacts.
77+
for {
78+
artifacts, resp, err := client.Actions.ListWorkflowRunArtifacts(ctx, owner, repo, githubRunID, opts)
79+
if err != nil {
80+
return "", fmt.Errorf("error listing artifacts: %w", err)
81+
}
82+
83+
allArtifacts = append(allArtifacts, artifacts.Artifacts...)
84+
85+
if resp.NextPage == 0 {
86+
break
87+
}
88+
opts.Page = resp.NextPage
89+
}
90+
91+
// Search for the artifact by name.
92+
for _, artifact := range allArtifacts {
93+
if artifact.GetName() == artifactName {
94+
artifactID := artifact.GetID()
95+
artifactURL := fmt.Sprintf("https://github.com/%s/%s/actions/runs/%d/artifacts/%d",
96+
owner, repo, githubRunID, artifactID)
97+
return artifactURL, nil
98+
}
99+
}
100+
101+
return "", fmt.Errorf("artifact '%s' not found in the workflow run", artifactName)
102+
}
103+
104+
// fetchArtifactLinkWithRetry attempts to fetch the artifact link with retry logic.
105+
func fetchArtifactLinkWithRetry(githubToken, githubRepo string, githubRunID int64, artifactName string, maxRetries int, delay time.Duration) (string, error) {
106+
var lastErr error
107+
for attempt := 1; attempt <= maxRetries; attempt++ {
108+
link, err := fetchArtifactLink(githubToken, githubRepo, githubRunID, artifactName)
109+
if err == nil {
110+
return link, nil
111+
}
112+
113+
lastErr = err
114+
if attempt == maxRetries {
115+
break
116+
}
117+
118+
log.Printf("[Attempt %d/%d] Artifact not yet available. Retrying in %s...", attempt, maxRetries, delay)
119+
time.Sleep(delay)
120+
}
121+
122+
return "", fmt.Errorf("failed to fetch artifact link after %d retries: %w", maxRetries, lastErr)
123+
}

0 commit comments

Comments
 (0)