From 7bef993e8225cb792a0d36b07b9958d6ee2a27e3 Mon Sep 17 00:00:00 2001 From: tijmenstor <23655598+tijmenstor@users.noreply.github.com> Date: Fri, 11 Oct 2024 11:35:28 +0200 Subject: [PATCH] feat: Add support for GitHub Check Runs Adds support to be able to update GitHub Check Runs, which are more powerful than GitHub Statuses [Link to issue in `argoproj/argo-cd`](https://github.com/argoproj/argo-cd/issues/19256) --- pkg/services/github.go | 166 ++++++++++++++++++++++++++++++++++++ pkg/services/github_test.go | 60 +++++++++++++ 2 files changed, 226 insertions(+) diff --git a/pkg/services/github.go b/pkg/services/github.go index e25af5f2..de79b3e5 100644 --- a/pkg/services/github.go +++ b/pkg/services/github.go @@ -35,6 +35,7 @@ type GitHubNotification struct { repoURL string revision string Status *GitHubStatus `json:"status,omitempty"` + CheckRun *GitHubCheckRun `json:"checkRun,omitempty"` Deployment *GitHubDeployment `json:"deployment,omitempty"` PullRequestComment *GitHubPullRequestComment `json:"pullRequestComment,omitempty"` RepoURLPath string `json:"repoURLPath,omitempty"` @@ -47,6 +48,20 @@ type GitHubStatus struct { TargetURL string `json:"targetURL,omitempty"` } +type GitHubCheckRun struct { + Name string `json:"name,omitempty"` + Status string `json:"status,omitempty"` + Conclusion string `json:"conclusion,omitempty"` + DetailsURL string `json:"detailsURL,omitempty"` + Output *GitHubCheckRunOutput `json:"output,omitempty"` +} + +type GitHubCheckRunOutput struct { + Title string `json:"title,omitempty"` + Summary string `json:"summary,omitempty"` + Text string `json:"text,omitempty"` +} + type GitHubDeployment struct { State string `json:"state,omitempty"` Environment string `json:"environment,omitempty"` @@ -85,6 +100,47 @@ func (g *GitHubNotification) GetTemplater(name string, f texttemplate.FuncMap) ( return nil, err } + var checkRunName, checkRunStatus, conclusion, detailsURL, title, summary, text *texttemplate.Template + if g.CheckRun != nil { + checkRunName, err = texttemplate.New(name).Funcs(f).Parse(g.CheckRun.Name) + if err != nil { + return nil, err + } + + checkRunStatus, err = texttemplate.New(name).Funcs(f).Parse(g.CheckRun.Status) + if err != nil { + return nil, err + } + + conclusion, err = texttemplate.New(name).Funcs(f).Parse(g.CheckRun.Conclusion) + if err != nil { + return nil, err + } + + detailsURL, err = texttemplate.New(name).Funcs(f).Parse(g.CheckRun.DetailsURL) + if err != nil { + return nil, err + } + + if g.CheckRun.Output != nil { + title, err = texttemplate.New(name).Funcs(f).Parse(g.CheckRun.Output.Title) + if err != nil { + return nil, err + } + + summary, err = texttemplate.New(name).Funcs(f).Parse(g.CheckRun.Output.Summary) + if err != nil { + return nil, err + } + + text, err = texttemplate.New(name).Funcs(f).Parse(g.CheckRun.Output.Text) + if err != nil { + return nil, err + } + } + + } + var statusState, label, targetURL *texttemplate.Template if g.Status != nil { statusState, err = texttemplate.New(name).Funcs(f).Parse(g.Status.State) @@ -159,6 +215,60 @@ func (g *GitHubNotification) GetTemplater(name string, f texttemplate.FuncMap) ( } notification.GitHub.revision = revisionData.String() + if g.CheckRun != nil { + if notification.GitHub.CheckRun == nil { + notification.GitHub.CheckRun = &GitHubCheckRun{} + } + + var nameData bytes.Buffer + if err := checkRunName.Execute(&nameData, vars); err != nil { + return err + } + notification.GitHub.CheckRun.Name = nameData.String() + + var statusData bytes.Buffer + if err := checkRunStatus.Execute(&statusData, vars); err != nil { + return err + } + notification.GitHub.CheckRun.Status = statusData.String() + + var conclusionData bytes.Buffer + if err := conclusion.Execute(&conclusionData, vars); err != nil { + return err + } + notification.GitHub.CheckRun.Conclusion = conclusionData.String() + + var detailsURLData bytes.Buffer + if err := detailsURL.Execute(&detailsURLData, vars); err != nil { + return err + } + notification.GitHub.CheckRun.DetailsURL = detailsURLData.String() + + if g.CheckRun.Output != nil { + if notification.GitHub.CheckRun.Output == nil { + notification.GitHub.CheckRun.Output = &GitHubCheckRunOutput{} + } + + var titleData bytes.Buffer + if err := title.Execute(&titleData, vars); err != nil { + return err + } + notification.GitHub.CheckRun.Output.Title = titleData.String() + + var summaryData bytes.Buffer + if err := summary.Execute(&summaryData, vars); err != nil { + return err + } + notification.GitHub.CheckRun.Output.Summary = summaryData.String() + + var textData bytes.Buffer + if err := text.Execute(&textData, vars); err != nil { + return err + } + notification.GitHub.CheckRun.Output.Text = textData.String() + } + } + if g.Status != nil { if notification.GitHub.Status == nil { notification.GitHub.Status = &GitHubStatus{} @@ -346,6 +456,62 @@ func (g gitHubService) Send(notification Notification, _ Destination) error { } } + if notification.GitHub.CheckRun != nil { + checkRuns, _, err := g.client.Checks.ListCheckRunsForRef( + context.Background(), + u[0], + u[1], + notification.GitHub.revision, + &github.ListCheckRunsOptions{ + CheckName: ¬ification.GitHub.CheckRun.Name, + }, + ) + + if err != nil { + return err + } + + var checkRun *github.CheckRun + if len(checkRuns.CheckRuns) != 0 { + checkRun = checkRuns.CheckRuns[0] + } else { + checkRun, _, err = g.client.Checks.CreateCheckRun( + context.Background(), + u[0], + u[1], + github.CreateCheckRunOptions{ + Name: notification.GitHub.CheckRun.Name, + HeadSHA: notification.GitHub.revision, + }, + ) + if err != nil { + return err + } + } + + g.client.Checks.UpdateCheckRun( + context.Background(), + u[0], + u[1], + *checkRun.ID, + github.UpdateCheckRunOptions{ + Name: notification.GitHub.CheckRun.Name, + DetailsURL: ¬ification.GitHub.CheckRun.DetailsURL, + Status: ¬ification.GitHub.CheckRun.Status, + Conclusion: ¬ification.GitHub.CheckRun.Conclusion, + Output: &github.CheckRunOutput{ + Title: ¬ification.GitHub.CheckRun.Output.Title, + Summary: ¬ification.GitHub.CheckRun.Output.Summary, + Text: ¬ification.GitHub.CheckRun.Output.Text, + }, + }, + ) + + if err != nil { + return err + } + } + if notification.GitHub.Deployment != nil { // maximum is 140 characters description := trunc(notification.Message, 140) diff --git a/pkg/services/github_test.go b/pkg/services/github_test.go index c9649974..5ce6d348 100644 --- a/pkg/services/github_test.go +++ b/pkg/services/github_test.go @@ -181,6 +181,66 @@ func TestGetTemplater_GitHub_Deployment(t *testing.T) { assert.Equal(t, "v0.0.1", notification.GitHub.Deployment.Reference) } +func TestGetTemplater_GitHub_CheckRun(t *testing.T) { + n := Notification{ + GitHub: &GitHubNotification{ + RepoURLPath: "{{.sync.spec.git.repo}}", + RevisionPath: "{{.sync.status.lastSyncedCommit}}", + CheckRun: &GitHubCheckRun{ + Name: "test-check-run", + Status: "completed", + Conclusion: "success", + DetailsURL: "https://example.com/details", + Output: &GitHubCheckRunOutput{ + Title: "Check Run Title", + Summary: "Check Run Summary", + Text: "Check Run Text", + }, + }, + }, + } + templater, err := n.GetTemplater("", template.FuncMap{}) + + if !assert.NoError(t, err) { + t.Fatalf("Failed to get templater: %v", err) + return + } + + var notification Notification + err = templater(¬ification, map[string]interface{}{ + "sync": map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "root-sync-test", + }, + "spec": map[string]interface{}{ + "git": map[string]interface{}{ + "repo": "https://github.com/argoproj-labs/argocd-notifications.git", + }, + }, + "status": map[string]interface{}{ + "lastSyncedCommit": "0123456789", + }, + }, + }) + + if !assert.NoError(t, err) { + t.Fatalf("Failed to execute templater: %v", err) + return + } + + assert.Equal(t, "{{.sync.spec.git.repo}}", notification.GitHub.RepoURLPath) + assert.Equal(t, "{{.sync.status.lastSyncedCommit}}", notification.GitHub.RevisionPath) + assert.Equal(t, "https://github.com/argoproj-labs/argocd-notifications.git", notification.GitHub.repoURL) + assert.Equal(t, "0123456789", notification.GitHub.revision) + assert.Equal(t, "test-check-run", notification.GitHub.CheckRun.Name) + assert.Equal(t, "completed", notification.GitHub.CheckRun.Status) + assert.Equal(t, "success", notification.GitHub.CheckRun.Conclusion) + assert.Equal(t, "https://example.com/details", notification.GitHub.CheckRun.DetailsURL) + assert.Equal(t, "Check Run Title", notification.GitHub.CheckRun.Output.Title) + assert.Equal(t, "Check Run Summary", notification.GitHub.CheckRun.Output.Summary) + assert.Equal(t, "Check Run Text", notification.GitHub.CheckRun.Output.Text) +} + func TestNewGitHubService_GitHubOptions(t *testing.T) { tests := []struct { name string