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 2 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
9 changes: 9 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
UNRELEASED
==========

Features
--------

- Allow configuration of the body of created Github/Gitlab issues via a template in the configuration file. ([\#84](https://github.com/matrix-org/rageshake/issues/84))


1.11.0 (2023-08-11)
===================

Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,29 @@ Optional parameters:
* `-listen <address>`: TCP network address to listen for HTTP requests
on. Example: `:9110`.

## 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. |

## HTTP endpoints

The following HTTP endpoints are exposed:
Expand Down
40 changes: 39 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"net/http"
"os"
"strings"
"text/template"
"time"

"github.com/google/go-github/github"
Expand All @@ -37,6 +38,20 @@ import (
"gopkg.in/yaml.v2"
)

// DefaultIssueBodyTemplate is the default template used for `issue_body_template` in the config.
//
// !!! Keep in step with the documentation in `rageshake.sample.yaml` !!!
richvdh marked this conversation as resolved.
Show resolved Hide resolved
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 }}
`

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 All @@ -63,6 +78,8 @@ type config struct {
GitlabProjectLabels map[string][]string `yaml:"gitlab_project_labels"`
GitlabIssueConfidential bool `yaml:"gitlab_issue_confidential"`

IssueBodyTemplate string `yaml:"issue_body_template"`

SlackWebhookURL string `yaml:"slack_webhook_url"`

EmailAddresses []string `yaml:"email_addresses"`
Expand Down Expand Up @@ -158,7 +175,16 @@ func main() {
log.Printf("Using %s/listing as public URI", apiPrefix)

rand.Seed(time.Now().UnixNano())
http.Handle("/api/submit", &submitServer{ghClient, glClient, apiPrefix, slack, genericWebhookClient, appNameMap, cfg})
http.Handle("/api/submit", &submitServer{
issueTemplate: parseIssueTemplate(cfg),
ghClient: ghClient,
glClient: glClient,
apiPrefix: apiPrefix,
slack: slack,
genericWebhookClient: genericWebhookClient,
allowedAppNameMap: appNameMap,
cfg: cfg,
})

// Make sure bugs directory exists
_ = os.Mkdir("bugs", os.ModePerm)
Expand Down Expand Up @@ -186,6 +212,18 @@ func main() {
log.Fatal(http.ListenAndServe(*bindAddr, nil))
}

func parseIssueTemplate(cfg *config) *template.Template {
issueTemplate := cfg.IssueBodyTemplate
if issueTemplate == "" {
issueTemplate = DefaultIssueBodyTemplate
}
parsedIssueTemplate, err := template.New("issue").Parse(issueTemplate)
if err != nil {
log.Fatalf("Invalid `issue_template` in config file: %s", err)
}
return parsedIssueTemplate
}

func configureAppNameMap(cfg *config) map[string]bool {
if len(cfg.AllowedAppNames) == 0 {
fmt.Println("Warning: allowed_app_names is empty. Accepting requests from all app names")
Expand Down
14 changes: 13 additions & 1 deletion rageshake.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ 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 }}
richvdh marked this conversation as resolved.
Show resolved Hide resolved

{{ 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 @@ -55,7 +68,6 @@ smtp_server: localhost:25
smtp_username: myemailuser
smtp_password: myemailpass


# a list of webhook URLs, (see docs/generic_webhook.md)
generic_webhook_urls:
- https://server.example.com/your-server/api
Expand Down
71 changes: 47 additions & 24 deletions submit.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"sort"
"strconv"
"strings"
"text/template"
"time"

"github.com/google/go-github/github"
Expand All @@ -47,6 +48,9 @@ import (
var maxPayloadSize = 1024 * 1024 * 55 // 55 MB

type submitServer struct {
// Template for building github and gitlab issues
issueTemplate *template.Template

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

// `issueBodyTemplatePayload` contains the data made available to the `issue_body_template`.
//
// !!! Keep in step with the documentation in `README.md` !!!
type issueBodyTemplatePayload struct {
payload
// Complete link to the listing URL that contains all uploaded logs
ListingURL string
}

// Stores additional information created during processing of a payload
type genericWebhookPayload struct {
payload
Expand All @@ -87,7 +100,10 @@ type genericWebhookPayload struct {
ListingURL string `json:"listing_url"`
}

// Stores information about a request made to this server
// `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` !!!
type payload struct {
// A unique ID for this payload, generated within this server
ID string `json:"id"`
Expand Down Expand Up @@ -580,9 +596,12 @@ func (s *submitServer) submitGithubIssue(ctx context.Context, p payload, listing
}
owner, repo := splits[0], splits[1]

issueReq := buildGithubIssueRequest(p, listingURL)
issueReq, err := buildGithubIssueRequest(p, listingURL, s.issueTemplate)
if err != nil {
return err
}

issue, _, err := s.ghClient.Issues.Create(ctx, owner, repo, &issueReq)
issue, _, err := s.ghClient.Issues.Create(ctx, owner, repo, issueReq)
if err != nil {
return err
}
Expand All @@ -602,7 +621,10 @@ func (s *submitServer) submitGitlabIssue(p payload, listingURL string, resp *sub
glProj := s.cfg.GitlabProjectMappings[p.AppName]
glLabels := s.cfg.GitlabProjectLabels[p.AppName]

issueReq := buildGitlabIssueRequest(p, listingURL, glLabels, s.cfg.GitlabIssueConfidential)
issueReq, err := buildGitlabIssueRequest(p, listingURL, s.issueTemplate, glLabels, s.cfg.GitlabIssueConfidential)
if err != nil {
return err
}

issue, _, err := s.glClient.Issues.CreateIssue(glProj, issueReq)

Expand Down Expand Up @@ -665,46 +687,47 @@ func buildReportBody(p payload, newline, quoteChar string) *bytes.Buffer {
return &bodyBuf
}

func buildGenericIssueRequest(p payload, listingURL string) (title, body string) {
bodyBuf := buildReportBody(p, " \n", "`")
richvdh marked this conversation as resolved.
Show resolved Hide resolved
func buildGenericIssueRequest(p payload, listingURL string, bodyTemplate *template.Template) (title, body string, err error) {
var bodyBuf bytes.Buffer

// Add log links to the body
fmt.Fprintf(bodyBuf, "\n[Logs](%s)", listingURL)
fmt.Fprintf(bodyBuf, " ([archive](%s))", listingURL+"?format=tar.gz")
issuePayload := issueBodyTemplatePayload{
payload: p,
ListingURL: listingURL,
}

for _, file := range p.Files {
fmt.Fprintf(
bodyBuf,
" / [%s](%s)",
file,
listingURL+"/"+file,
)
if err = bodyTemplate.Execute(&bodyBuf, issuePayload); err != nil {
return
}

title = buildReportTitle(p)

body = bodyBuf.String()

return
}

func buildGithubIssueRequest(p payload, listingURL string) github.IssueRequest {
title, body := buildGenericIssueRequest(p, listingURL)
func buildGithubIssueRequest(p payload, listingURL string, bodyTemplate *template.Template) (*github.IssueRequest, error) {
title, body, err := buildGenericIssueRequest(p, listingURL, bodyTemplate)
if err != nil {
return nil, err
}

labels := p.Labels
// go-github doesn't like nils
if labels == nil {
labels = []string{}
}
return github.IssueRequest{
return &github.IssueRequest{
Title: &title,
Body: &body,
Labels: &labels,
}
}, nil
}

func buildGitlabIssueRequest(p payload, listingURL string, labels []string, confidential bool) *gitlab.CreateIssueOptions {
title, body := buildGenericIssueRequest(p, listingURL)
func buildGitlabIssueRequest(p payload, listingURL string, bodyTemplate *template.Template, labels []string, confidential bool) (*gitlab.CreateIssueOptions, error) {
title, body, err := buildGenericIssueRequest(p, listingURL, bodyTemplate)
if err != nil {
return nil, err
}

if p.Labels != nil {
labels = append(labels, p.Labels...)
Expand All @@ -715,7 +738,7 @@ func buildGitlabIssueRequest(p payload, listingURL string, labels []string, conf
Description: &body,
Confidential: &confidential,
Labels: labels,
}
}, nil
}

func (s *submitServer) sendEmail(p payload, reportDir string) error {
Expand Down
Loading
Loading