Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate emails and github/gitlab issue bodies from a template #84

Merged
merged 8 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
22 changes: 2 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,8 @@ Optional parameters:

## Issue template

It is possible to specify a template in the configuration file which will be used to build the
body of any issues created on Github or Gitlab, via the `issue_body_template` setting.
See [rageshake.sample.yaml](rageshake.sample.yaml) for an example.

See https://pkg.go.dev/text/template#pkg-overview for documentation of the template language.

The following properties are defined on the input (accessible via `.` or `$`):

| Name | Type | Description |
|--------------|---------------------|---------------------------------------------------------------------------------------------------|
| `ID` | `string` | The unique ID for this rageshake. |
| `UserText` | `string` | A multi-line string containing the user description of the fault (from `text` in the submission). |
| `AppName` | `string` | A short slug to identify the app making the report (from `app` in the submission). |
| `Labels` | `[]string` | A list of labels requested by the application. |
| `Data` | `map[string]string` | A map of other key/value pairs included in the submission. |
| `Logs` | `[]string` | A list of log file names. |
| `LogErrors` | `[]string` | Set if there are log parsing errors. |
| `Files` | `[]string` | A list of other files (not logs) uploaded as part of the rageshake. |
| `FileErrors` | `[]string` | Set if there are file parsing errors. |
| `ListingURL` | `string` | Complete link to the listing URL that contains all uploaded logs. |
It is possible to override the templates used to construct emails, and Github and Gitlab issues.
See [templates/README.md](templates/README.md) for more information.

## HTTP endpoints

Expand Down
51 changes: 30 additions & 21 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,17 @@ import (

"gopkg.in/yaml.v2"
)
import _ "embed"

// DefaultIssueBodyTemplate is the default template used for `issue_body_template` in the config.
// DefaultIssueBodyTemplate is the default template used for `issue_body_template_file` in the config.
//
// !!! Keep in step with the documentation in `rageshake.sample.yaml` !!!
const DefaultIssueBodyTemplate = `User message:
{{ .UserText }}

{{ range $key, $val := .Data -}}
{{ $key }}: ` + "`{{ $val }}`" + `
{{ end }}
[Logs]({{ .ListingURL }}) ([archive]({{ .ListingURL }}?format=tar.gz))
{{- range $file := .Files}} / [{{ $file }}]({{ $.ListingURL }}/{{ $file }})
{{- end }}
`
//go:embed templates/issue_body.tmpl
var DefaultIssueBodyTemplate string

// DefaultEmailBodyTemplate is the default template used for `email_body_template_file` in the config.
//
//go:embed templates/email_body.tmpl
var DefaultEmailBodyTemplate string

var configPath = flag.String("config", "rageshake.yaml", "The path to the config file. For more information, see the config file in this repository.")
var bindAddr = flag.String("listen", ":9110", "The port to listen on.")
Expand Down Expand Up @@ -78,7 +75,8 @@ type config struct {
GitlabProjectLabels map[string][]string `yaml:"gitlab_project_labels"`
GitlabIssueConfidential bool `yaml:"gitlab_issue_confidential"`

IssueBodyTemplate string `yaml:"issue_body_template"`
IssueBodyTemplateFile string `yaml:"issue_body_template_file"`
EmailBodyTemplateFile string `yaml:"email_body_template_file"`

SlackWebhookURL string `yaml:"slack_webhook_url"`

Expand Down Expand Up @@ -176,7 +174,8 @@ func main() {

rand.Seed(time.Now().UnixNano())
http.Handle("/api/submit", &submitServer{
issueTemplate: parseIssueTemplate(cfg),
issueTemplate: parseTemplate(DefaultIssueBodyTemplate, cfg.IssueBodyTemplateFile, "issue"),
emailTemplate: parseTemplate(DefaultEmailBodyTemplate, cfg.EmailBodyTemplateFile, "email"),
ghClient: ghClient,
glClient: glClient,
apiPrefix: apiPrefix,
Expand Down Expand Up @@ -212,16 +211,26 @@ func main() {
log.Fatal(http.ListenAndServe(*bindAddr, nil))
}

func parseIssueTemplate(cfg *config) *template.Template {
issueTemplate := cfg.IssueBodyTemplate
if issueTemplate == "" {
issueTemplate = DefaultIssueBodyTemplate
// parseTemplate parses a template file, with fallback to default.
//
// If `configFileSettingValue` is non-empty, it is used as the name of a file to read. Otherwise, `defaultTemplate` is
// used.
//
// The template text is then parsed into a template named `templateName`.
func parseTemplate(defaultTemplate string, configFileSettingValue string, templateName string) *template.Template {
richvdh marked this conversation as resolved.
Show resolved Hide resolved
templateText := defaultTemplate
if configFileSettingValue != "" {
issueTemplateBytes, err := os.ReadFile(configFileSettingValue)
if err != nil {
log.Fatalf("Unable to read template file `%s`: %s", configFileSettingValue, err)
}
templateText = string(issueTemplateBytes)
}
parsedIssueTemplate, err := template.New("issue").Parse(issueTemplate)
parsedTemplate, err := template.New(templateName).Parse(templateText)
if err != nil {
log.Fatalf("Invalid `issue_template` in config file: %s", err)
log.Fatalf("Invalid template file %s in config file: %s", configFileSettingValue, err)
}
return parsedIssueTemplate
return parsedTemplate
}

func configureAppNameMap(cfg *config) map[string]bool {
Expand Down
18 changes: 5 additions & 13 deletions rageshake.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,6 @@ github_token: secrettoken
github_project_mappings:
my-app: octocat/HelloWorld

# A template for the body of Github and Gitlab issues. The default template is as shown below.
#
# See `README.md` for more information on what can be specified here.
issue_body_template: |
{{ .UserText }}

{{ range $key, $val := .Data -}}
{{ $key }}: `{{ $val }}`
{{ end }}
[Logs]({{ .ListingURL }}) ([archive]({{ .ListingURL }}?format=tar.gz))
{{- range $file := .Files}} / [{{ $file }}]({{ $.ListingURL }}/{{ $file }})
{{- end }}

# a GitLab personal access token (https://gitlab.com/-/profile/personal_access_tokens), which
# will be used to create a GitLab issue for each report. It requires
# `api` scope. If omitted, no issues will be created.
Expand Down Expand Up @@ -72,3 +59,8 @@ smtp_password: myemailpass
generic_webhook_urls:
- https://server.example.com/your-server/api
- http://another-server.com/api

# The paths of template files for the body of Github and Gitlab issues, and emails.
# See `templates/README.md` for more information.
issue_body_template_file: path/to/issue_body.tmpl
email_body_template_file: path/to/email_body.tmpl
53 changes: 23 additions & 30 deletions submit.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ type submitServer struct {
// Template for building github and gitlab issues
issueTemplate *template.Template

// Template for building emails
emailTemplate *template.Template

// github client for reporting bugs. may be nil, in which case,
// reporting is disabled.
ghClient *github.Client
Expand Down Expand Up @@ -82,9 +85,10 @@ type jsonLogEntry struct {
Lines string `json:"lines"`
}

// `issueBodyTemplatePayload` contains the data made available to the `issue_body_template`.
// `issueBodyTemplatePayload` contains the data made available to the `issue_body_template` and
// `email_body_template`.
//
// !!! Keep in step with the documentation in `README.md` !!!
// !!! Keep in step with the documentation in `templates/README.md` !!!
type issueBodyTemplatePayload struct {
payload
// Complete link to the listing URL that contains all uploaded logs
Expand All @@ -103,7 +107,7 @@ type genericWebhookPayload struct {
// `payload` stores information about a request made to this server.
//
// !!! Since this is inherited by `issueBodyTemplatePayload`, remember to keep it in step
// with the documentation in `README.md` !!!
// with the documentation in `templates/README.md` !!!
type payload struct {
// A unique ID for this payload, generated within this server
ID string `json:"id"`
Expand Down Expand Up @@ -521,7 +525,7 @@ func (s *submitServer) saveReport(ctx context.Context, p payload, reportDir, lis
return nil, err
}

if err := s.sendEmail(p, reportDir); err != nil {
if err := s.sendEmail(p, reportDir, listingURL); err != nil {
return nil, err
}

Expand Down Expand Up @@ -671,23 +675,7 @@ func buildReportTitle(p payload) string {
return trimmedUserText
}

func buildReportBody(p payload, newline, quoteChar string) *bytes.Buffer {
var bodyBuf bytes.Buffer
fmt.Fprintf(&bodyBuf, "User message:\n\n%s\n\n", p.UserText)
var dataKeys []string
for k := range p.Data {
dataKeys = append(dataKeys, k)
}
sort.Strings(dataKeys)
for _, k := range dataKeys {
v := p.Data[k]
fmt.Fprintf(&bodyBuf, "%s: %s%s%s%s", k, quoteChar, v, quoteChar, newline)
}

return &bodyBuf
}

func buildGenericIssueRequest(p payload, listingURL string, bodyTemplate *template.Template) (title, body string, err error) {
func buildGenericIssueRequest(p payload, listingURL string, bodyTemplate *template.Template) (title string, body []byte, err error) {
var bodyBuf bytes.Buffer

issuePayload := issueBodyTemplatePayload{
Expand All @@ -700,7 +688,7 @@ func buildGenericIssueRequest(p payload, listingURL string, bodyTemplate *templa
}

title = buildReportTitle(p)
body = bodyBuf.String()
body = bodyBuf.Bytes()

return
}
Expand All @@ -716,9 +704,10 @@ func buildGithubIssueRequest(p payload, listingURL string, bodyTemplate *templat
if labels == nil {
labels = []string{}
}
bodyStr := string(body)
return &github.IssueRequest{
Title: &title,
Body: &body,
Body: &bodyStr,
Labels: &labels,
}, nil
}
Expand All @@ -733,19 +722,25 @@ func buildGitlabIssueRequest(p payload, listingURL string, bodyTemplate *templat
labels = append(labels, p.Labels...)
}

bodyStr := string(body)
return &gitlab.CreateIssueOptions{
Title: &title,
Description: &body,
Description: &bodyStr,
Confidential: &confidential,
Labels: labels,
}, nil
}

func (s *submitServer) sendEmail(p payload, reportDir string) error {
func (s *submitServer) sendEmail(p payload, reportDir string, listingURL string) error {
if len(s.cfg.EmailAddresses) == 0 {
return nil
}

title, body, err := buildGenericIssueRequest(p, listingURL, s.emailTemplate)
if err != nil {
return err
}

e := email.NewEmail()

e.From = "Rageshake <rageshake@matrix.org>"
Expand All @@ -754,10 +749,8 @@ func (s *submitServer) sendEmail(p payload, reportDir string) error {
}

e.To = s.cfg.EmailAddresses

e.Subject = fmt.Sprintf("[%s] %s", p.AppName, buildReportTitle(p))

e.Text = buildReportBody(p, "\n", "\"").Bytes()
e.Subject = fmt.Sprintf("[%s] %s", p.AppName, title)
e.Text = body

allFiles := append(p.Files, p.Logs...)
for _, file := range allFiles {
Expand All @@ -769,7 +762,7 @@ func (s *submitServer) sendEmail(p payload, reportDir string) error {
if s.cfg.SMTPPassword != "" || s.cfg.SMTPUsername != "" {
auth = smtp.PlainAuth("", s.cfg.SMTPUsername, s.cfg.SMTPPassword, s.cfg.SMTPServer)
}
err := e.Send(s.cfg.SMTPServer, auth)
err = e.Send(s.cfg.SMTPServer, auth)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion submit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func submitSimpleRequestToServer(t *testing.T, allowedAppNameMap map[string]bool
w := httptest.NewRecorder()

var cfg config
s := &submitServer{nil, nil, nil, "/", nil, nil, allowedAppNameMap, &cfg}
s := &submitServer{nil, nil, nil, nil, "/", nil, nil, allowedAppNameMap, &cfg}

s.ServeHTTP(w, req)
rsp := w.Result()
Expand Down
26 changes: 26 additions & 0 deletions templates/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
This directory contains the default templates that are used by the rageshake server.

The templates can be overridden via settings in the config file.

The templates are as follows:

* `issue_body.tmpl`: Used when filing an issue at Github or Gitlab, and gives the issue description. Override via
the `issue_body_template_file` setting in the configuration file.
* `email_body.tmpl`: Used when sending an email. Override via the `email_body_template_file` configuration setting.

See https://pkg.go.dev/text/template#pkg-overview for documentation of the template language.

The following properties are defined on the input (accessible via `.` or `$`):

| Name | Type | Description |
|--------------|---------------------|---------------------------------------------------------------------------------------------------|
| `ID` | `string` | The unique ID for this rageshake. |
| `UserText` | `string` | A multi-line string containing the user description of the fault (from `text` in the submission). |
| `AppName` | `string` | A short slug to identify the app making the report (from `app` in the submission). |
| `Labels` | `[]string` | A list of labels requested by the application. |
| `Data` | `map[string]string` | A map of other key/value pairs included in the submission. |
| `Logs` | `[]string` | A list of log file names. |
| `LogErrors` | `[]string` | Set if there are log parsing errors. |
| `Files` | `[]string` | A list of other files (not logs) uploaded as part of the rageshake. |
| `FileErrors` | `[]string` | Set if there are file parsing errors. |
| `ListingURL` | `string` | Complete link to the listing URL that contains all uploaded logs. |
9 changes: 9 additions & 0 deletions templates/email_body.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
User message:
{{ .UserText }}

{{ range $key, $val := .Data -}}
{{ $key }}: "{{ $val }}"
{{ end }}
[Logs]({{ .ListingURL }}) ([archive]({{ .ListingURL }}?format=tar.gz))
{{- range $file := .Files}} / [{{ $file }}]({{ $.ListingURL }}/{{ $file }})
{{- end }}
9 changes: 9 additions & 0 deletions templates/issue_body.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
User message:
{{ .UserText }}

{{ range $key, $val := .Data -}}
{{ $key }}: `{{ $val }}`
{{ end }}
[Logs]({{ .ListingURL }}) ([archive]({{ .ListingURL }}?format=tar.gz))
{{- range $file := .Files}} / [{{ $file }}]({{ $.ListingURL }}/{{ $file }})
{{- end }}
Loading