From a43d3d17e100dd2400dca4969acd159ba7f1cb84 Mon Sep 17 00:00:00 2001 From: Yazdan Mohammadi Date: Wed, 23 Oct 2024 14:56:38 +0200 Subject: [PATCH] Custom Commit Status URL (#30) * return err as second value * Make executeTemplate more general by extracting template path join * Generate target url with dynamic value (CommitTime) * Inject /etc/telefonistka-gh-app-config/ for mirrord This will be used for CUSTOM_COMMIT_STATUS_URL_TEMPLATE_PATH * Add doc * Add test * Fix lint issues * Make targetURL const * Change the level to Debug See: https://github.com/commercetools/telefonistka/pull/30#discussion_r1810861768 --- docs/installation.md | 6 ++ internal/pkg/githubapi/github.go | 55 +++++++++++++++---- internal/pkg/githubapi/github_test.go | 49 +++++++++++++++++ internal/pkg/githubapi/promotion.go | 2 +- ...stom_commit_status_invalid_template.gotmpl | 1 + ...custom_commit_status_valid_template.gotmpl | 1 + mirrord.json | 2 +- 7 files changed, 103 insertions(+), 13 deletions(-) create mode 100644 internal/pkg/githubapi/testdata/custom_commit_status_invalid_template.gotmpl create mode 100644 internal/pkg/githubapi/testdata/custom_commit_status_valid_template.gotmpl diff --git a/docs/installation.md b/docs/installation.md index 5b9b5771..3f454aa9 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -91,6 +91,12 @@ Environment variables for the webhook process: `TEMPLATES_PATH` Telefonistka uses Go templates to format GitHub PR comments, the variable override the default templates path("templates/"), useful for environments where the container workdir is overridden(like GitHub Actions) or when custom templates are desired. +`CUSTOM_COMMIT_STATUS_URL_TEMPLATE_PATH` allows you to set a custom [commit status](https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#about-commit-statuses) target URL using Go templates. The commit time will be passed as a dynamic parameter to the template. Here is an example: + +```console +https://custom-url.com?time={{.CommitTime}} +``` + `ARGOCD_SERVER_ADDR` Hostname and port of the ArgoCD API endpoint, like `argocd-server.argocd.svc.cluster.local:443`, default is `localhost:8080"` `ARGOCD_TOKEN` ArgoCD authentication token. diff --git a/internal/pkg/githubapi/github.go b/internal/pkg/githubapi/github.go index f9be31a7..29430cb3 100644 --- a/internal/pkg/githubapi/github.go +++ b/internal/pkg/githubapi/github.go @@ -12,6 +12,7 @@ import ( "net/http" "os" "path" + "path/filepath" "regexp" "sort" "strings" @@ -238,7 +239,7 @@ func HandlePREvent(eventPayload *github.PullRequestEvent, ghPrClientDetails GhPr } func generateArgoCdDiffComments(diffCommentData DiffCommentData, githubCommentMaxSize int) (comments []string, err error) { - err, templateOutput := executeTemplate("argoCdDiff", "argoCD-diff-pr-comment.gotmpl", diffCommentData) + templateOutput, err := executeTemplate("argoCdDiff", defaultTemplatesFullPath("argoCD-diff-pr-comment.gotmpl"), diffCommentData) if err != nil { return nil, fmt.Errorf("failed to generate ArgoCD diff comment template: %w", err) } @@ -255,7 +256,7 @@ func generateArgoCdDiffComments(diffCommentData DiffCommentData, githubCommentMa componentTemplateData := diffCommentData componentTemplateData.DiffOfChangedComponents = []argocd.DiffResult{singleComponentDiff} componentTemplateData.Header = fmt.Sprintf("Component %d/%d: %s (Split for comment size)", i+1, totalComponents, singleComponentDiff.ComponentPath) - err, templateOutput := executeTemplate("argoCdDiff", "argoCD-diff-pr-comment.gotmpl", componentTemplateData) + templateOutput, err := executeTemplate("argoCdDiff", defaultTemplatesFullPath("argoCD-diff-pr-comment.gotmpl"), componentTemplateData) if err != nil { return nil, fmt.Errorf("failed to generate ArgoCD diff comment template: %w", err) } @@ -268,7 +269,7 @@ func generateArgoCdDiffComments(diffCommentData DiffCommentData, githubCommentMa } // now we don't have much choice, this is the saddest path, we'll use the concise template - err, templateOutput = executeTemplate("argoCdDiffConcise", "argoCD-diff-pr-comment-concise.gotmpl", componentTemplateData) + templateOutput, err = executeTemplate("argoCdDiffConcise", defaultTemplatesFullPath("argoCD-diff-pr-comment-concise.gotmpl"), componentTemplateData) if err != nil { return comments, fmt.Errorf("failed to generate ArgoCD diff comment template: %w", err) } @@ -497,7 +498,7 @@ func handleCommentPrEvent(ghPrClientDetails GhPrClientDetails, ce *github.IssueC } func commentPlanInPR(ghPrClientDetails GhPrClientDetails, promotions map[string]PromotionInstance) { - err, templateOutput := executeTemplate("dryRunMsg", "dry-run-pr-comment.gotmpl", promotions) + templateOutput, err := executeTemplate("dryRunMsg", defaultTemplatesFullPath("dry-run-pr-comment.gotmpl"), promotions) if err != nil { ghPrClientDetails.PrLogger.Errorf("Failed to generate dry-run comment template: err=%s\n", err) return @@ -505,17 +506,21 @@ func commentPlanInPR(ghPrClientDetails GhPrClientDetails, promotions map[string] _ = commentPR(ghPrClientDetails, templateOutput) } -func executeTemplate(templateName string, templateFile string, data interface{}) (error, string) { +func executeTemplate(templateName string, templateFile string, data interface{}) (string, error) { var templateOutput bytes.Buffer - messageTemplate, err := template.New(templateName).ParseFiles(getEnv("TEMPLATES_PATH", "templates/") + templateFile) + messageTemplate, err := template.New(templateName).ParseFiles(templateFile) if err != nil { - return fmt.Errorf("failed to parse template: %w", err), "" + return "", fmt.Errorf("failed to parse template: %w", err) } err = messageTemplate.ExecuteTemplate(&templateOutput, templateName, data) if err != nil { - return fmt.Errorf("failed to execute template: %w", err), "" + return "", fmt.Errorf("failed to execute template: %w", err) } - return nil, templateOutput.String() + return templateOutput.String(), nil +} + +func defaultTemplatesFullPath(templateFile string) string { + return filepath.Join(getEnv("TEMPLATES_PATH", "templates/") + templateFile) } func commentPR(ghPrClientDetails GhPrClientDetails, commentBody string) error { @@ -644,7 +649,7 @@ func handleMergedPrEvent(ghPrClientDetails GhPrClientDetails, prApproverGithubCl templateData := map[string]interface{}{ "prNumber": *pull.Number, } - err, templateOutput := executeTemplate("autoMerge", "auto-merge-comment.gotmpl", templateData) + templateOutput, err := executeTemplate("autoMerge", defaultTemplatesFullPath("auto-merge-comment.gotmpl"), templateData) if err != nil { return err } @@ -817,7 +822,7 @@ func SetCommitStatus(ghPrClientDetails GhPrClientDetails, state string) { context := "telefonistka" avatarURL := "https://avatars.githubusercontent.com/u/1616153?s=64" description := "Telefonistka GitOps Bot" - targetURL := "https://github.com/wayfair-incubator/telefonistka" + targetURL := commitStatusTargetURL(time.Now()) commitStatus := &github.RepoStatus{ TargetURL: &targetURL, @@ -1211,3 +1216,31 @@ func GetFileContent(ghPrClientDetails GhPrClientDetails, branch string, filePath } return fileContentString, resp.StatusCode, nil } + +// commitStatusTargetURL generates a target URL based on an optional +// template file specified by the environment variable CUSTOM_COMMIT_STATUS_URL_TEMPLATE_PATH. +// If the template file is not found or an error occurs during template execution, +// it returns a default URL. +// passed parameter commitTime can be used in the template as .CommitTime +func commitStatusTargetURL(commitTime time.Time) string { + const targetURL string = "https://github.com/wayfair-incubator/telefonistka" + + tmplFile := os.Getenv("CUSTOM_COMMIT_STATUS_URL_TEMPLATE_PATH") + tmplName := filepath.Base(tmplFile) + + // dynamic parameters to be used in the template + p := struct { + CommitTime time.Time + }{ + CommitTime: commitTime, + } + renderedURL, err := executeTemplate(tmplName, tmplFile, p) + if err != nil { + log.Debugf("Failed to render target URL template: %v", err) + return targetURL + } + + // trim any leading/trailing whitespace + renderedURL = strings.TrimSpace(renderedURL) + return renderedURL +} diff --git a/internal/pkg/githubapi/github_test.go b/internal/pkg/githubapi/github_test.go index 3814e2b8..8284258b 100644 --- a/internal/pkg/githubapi/github_test.go +++ b/internal/pkg/githubapi/github_test.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -289,3 +290,51 @@ func TestGhPrClientDetailsGetBlameURLPrefix(t *testing.T) { assert.Equal(t, tc.ExpectURL, blameURLPrefix) } } + +func TestCommitStatusTargetURL(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + expectedURL string + templateFile string + validTemplate bool + }{ + "Default URL when no env var is set": { + expectedURL: "https://github.com/wayfair-incubator/telefonistka", + templateFile: "", + validTemplate: false, + }, + "Custom URL from template": { + expectedURL: "https://custom-url.com?time=%d&calculated_time=%d", + templateFile: "./testdata/custom_commit_status_valid_template.gotmpl", + validTemplate: true, + }, + "Invalid template": { + expectedURL: "https://github.com/wayfair-incubator/telefonistka", + templateFile: "./testdata/custom_commit_status_invalid_template.gotmpl", + validTemplate: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + now := time.Now() + + expectedURL := tc.expectedURL + if tc.templateFile != "" { + os.Setenv("CUSTOM_COMMIT_STATUS_URL_TEMPLATE_PATH", tc.templateFile) + defer os.Unsetenv("CUSTOM_COMMIT_STATUS_URL_TEMPLATE_PATH") + + if tc.validTemplate { + expectedURL = fmt.Sprintf(expectedURL, now.UnixMilli(), now.Add(-10*time.Minute).UnixMilli()) + } + } + + result := commitStatusTargetURL(now) + if result != expectedURL { + t.Errorf("%s: Expected URL to be %q, got %q", name, expectedURL, result) + } + }) + } +} diff --git a/internal/pkg/githubapi/promotion.go b/internal/pkg/githubapi/promotion.go index 298d73b0..aac4edbe 100644 --- a/internal/pkg/githubapi/promotion.go +++ b/internal/pkg/githubapi/promotion.go @@ -76,7 +76,7 @@ func DetectDrift(ghPrClientDetails GhPrClientDetails) error { } } if len(diffOutputMap) != 0 { - err, templateOutput := executeTemplate("driftMsg", "drift-pr-comment.gotmpl", diffOutputMap) + templateOutput, err := executeTemplate("driftMsg", defaultTemplatesFullPath("drift-pr-comment.gotmpl"), diffOutputMap) if err != nil { return err } diff --git a/internal/pkg/githubapi/testdata/custom_commit_status_invalid_template.gotmpl b/internal/pkg/githubapi/testdata/custom_commit_status_invalid_template.gotmpl new file mode 100644 index 00000000..32e4e312 --- /dev/null +++ b/internal/pkg/githubapi/testdata/custom_commit_status_invalid_template.gotmpl @@ -0,0 +1 @@ +https://custom-url.com?time={{.InvalidField}} diff --git a/internal/pkg/githubapi/testdata/custom_commit_status_valid_template.gotmpl b/internal/pkg/githubapi/testdata/custom_commit_status_valid_template.gotmpl new file mode 100644 index 00000000..93bb82f6 --- /dev/null +++ b/internal/pkg/githubapi/testdata/custom_commit_status_valid_template.gotmpl @@ -0,0 +1 @@ +{{ $calculated_time := .CommitTime.Add -600000000000 }}https://custom-url.com?time={{.CommitTime.UnixMilli}}&calculated_time={{$calculated_time.UnixMilli}} diff --git a/mirrord.json b/mirrord.json index e0c54ee1..9472de46 100644 --- a/mirrord.json +++ b/mirrord.json @@ -10,7 +10,7 @@ "fs": { "mode": "read", "read_write": ".+\\.json" , - "read_only": [ "^/etc/telefonistka-gh-app-creds/.*" ] + "read_only": [ "^/etc/telefonistka-gh-app-creds/.*", "^/etc/telefonistka-gh-app-config/.*" ] }, "network": { "incoming": "steal",