1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: Apache-2.0
19 "git.arvados.org/arvados-dev.git/lib/redmine"
20 log "github.com/sirupsen/logrus"
21 "github.com/spf13/cobra"
22 "github.com/spf13/viper"
25 //go:embed emailTemplate.txt
26 var emailTemplate string
29 type ReviewTask struct {
46 SprintStartDate string
48 ReviewTasksInProgress []ReviewTask
49 UnassignedReviewTasks []ReviewTask
50 NewReviewTasks []ReviewTask
58 Endpoint string `json:"endpoint"` // https://dev-dev.arvados.org
59 Apikey string `json:"apikey"` // abcde...
62 func loadConfig() config {
66 Viper.SetEnvPrefix("redmine") // will be uppercased automatically
67 Viper.BindEnv("endpoint")
68 Viper.BindEnv("apikey")
70 c.Endpoint = Viper.GetString("endpoint")
71 c.Apikey = Viper.GetString("apikey")
77 rootCmd.PersistentFlags().StringP("output", "o", "", "Output format. Empty for human-readable, 'json' or 'json-line'")
78 rootCmd.PersistentFlags().BoolP("help", "h", false, "Print help")
79 rootCmd.PersistentFlags().BoolP("debug", "d", false, "Print debug output")
80 rootCmd.PersistentFlags().BoolP("send", "s", false, "Send reports via e-mail (if false, print them to stdout)")
81 rootCmd.Flags().StringP("project", "p", "", "Redmine project name")
82 err := rootCmd.MarkFlagRequired("project")
84 log.Fatalf(err.Error())
89 var rootCmd = &cobra.Command{
90 Use: "review-task-reminder",
91 Short: "review-task-reminder - Send e-mail reminders with the list of review tasks in progress",
93 review-task-reminder looks at the current sprint and e-mails a reminder to all
94 people with assigned review tasks.
96 https://git.arvados.org/arvados-dev.git/cmd/review-task-reminder` +
97 "\nThe REDMINE_ENDPOINT environment variable must be set to the base URL of your redmine server." +
98 "\nThe REDMINE_APIKEY environment variable must be set to your redmine API key.",
99 PreRunE: func(cmd *cobra.Command, args []string) error {
100 if conf.Endpoint == "" {
103 fmt.Println("Error: the REDMINE_ENDPOINT environment variable must be set to the base URL of your redmine server")
106 if conf.Apikey == "" {
109 fmt.Println("Error: the REDMINE_APIKEY environment variable must be set to your redmine API key")
113 debug, err = cmd.Flags().GetBool("debug")
115 log.Fatalf(err.Error())
118 // parse string, this is built-in feature of logrus
119 ll, err := log.ParseLevel("debug")
123 // set global log level
125 log.Debug("Enabled debug log level")
128 send, err = cmd.Flags().GetBool("send")
130 log.Fatalf(err.Error())
135 Run: func(cmd *cobra.Command, args []string) {
136 log.Debug("Creating redmine object")
137 rm := redmine.NewClient(conf.Endpoint, conf.Apikey)
139 log.Debug("Getting project object")
140 project, err := cmd.Flags().GetString("project")
142 log.Fatalf(err.Error())
144 p, err := rm.GetProjectByName(project)
146 log.Fatalf(err.Error())
149 log.Debugf("Project: %s ID: %d", project, p.ID)
151 log.Debug("Getting versions")
152 versions, err := rm.Versions(p.ID)
154 log.Fatalf(err.Error())
156 // Find any current sprint(s)
157 for _, v := range versions {
159 if v.Status != "open" {
162 // The due date must be in the future
166 dueDate, err := time.Parse("2006-01-02", v.DueDate)
168 log.Fatalf(err.Error())
170 if time.Now().After(dueDate) {
173 // The start date must be in the past (have to look up the Sprint object!)
174 log.Debugf("Getting sprint with id %d", v.ID)
175 s, err := rm.Sprint(v.ID)
177 log.Fatalf(err.Error())
179 if s.StartDate == "" {
182 startDate, err := time.Parse("2006-01-02", s.StartDate)
184 log.Fatalf(err.Error())
186 if time.Now().Before(startDate) {
189 // Found a current sprint
190 log.Debugf("Current sprint: %+#v", s)
192 // Get the issues from this sprint
193 var issueFilter redmine.IssueFilter
194 issueFilter.VersionID = strconv.Itoa(v.ID)
195 log.Debugf("Getting issues with version ID %d", v.ID)
196 issues, err := rm.FilteredIssues(&issueFilter)
198 log.Fatalf(err.Error())
201 log.Debugf("Retrieved %d issues", len(issues))
202 reviewTasksByDeveloper := make(map[int][]ReviewTask)
203 var UnassignedReviewTasks []ReviewTask
204 for _, i := range issues {
205 log.Debugf("Considering issue (%s): %+#v, \"%s\"", i.Tracker.Name, i.ID, i.Subject)
207 if i.Tracker.Name != "Task" {
210 // Filter for review tasks (issue subject must start with 'review')
211 reviewRE := regexp.MustCompile(`^review`)
212 if !reviewRE.MatchString(i.Subject) {
215 // Is the task assigned?
216 if i.AssignedTo == nil {
217 log.Debugf("Found unassigned review task: %+#v, \"%s\"", i.ID, i.Subject)
218 log.Debugf("Getting parent issue with ID %d", i.Parent.ID)
219 parent, err := rm.GetIssue(i.Parent.ID)
221 log.Fatalf(err.Error())
224 IssueID: "#" + strconv.Itoa(parent.ID),
225 IssueSubject: limit(parent.Subject, 58),
226 IssueURL: conf.Endpoint + "/issues/" + strconv.Itoa(parent.ID),
227 ID: "#" + strconv.Itoa(i.ID),
228 Subject: limit(i.Subject, 58),
229 URL: conf.Endpoint + "/issues/" + strconv.Itoa(i.ID),
230 Status: i.Status.Name,
232 UnassignedReviewTasks = append(UnassignedReviewTasks, rt)
235 // Found an assigned review task
236 log.Debugf("Found assigned review task: %+#v, \"%s\", assigned to %+#v", i.ID, i.Subject, i.AssignedTo.ID)
237 if _, ok := reviewTasksByDeveloper[i.AssignedTo.ID]; !ok {
238 reviewTasksByDeveloper[i.AssignedTo.ID] = []ReviewTask{}
240 log.Debugf("Getting parent issue with ID %d", i.Parent.ID)
241 parent, err := rm.GetIssue(i.Parent.ID)
243 log.Fatalf(err.Error())
246 IssueID: "#" + strconv.Itoa(parent.ID),
247 IssueSubject: limit(parent.Subject, 58),
248 IssueURL: conf.Endpoint + "/issues/" + strconv.Itoa(parent.ID),
249 ID: "#" + strconv.Itoa(i.ID),
250 Subject: limit(i.Subject, 58),
251 URL: conf.Endpoint + "/issues/" + strconv.Itoa(i.ID),
252 Status: i.Status.Name,
254 reviewTasksByDeveloper[i.AssignedTo.ID] = append(reviewTasksByDeveloper[i.AssignedTo.ID], rt)
257 // Create the report(s)
258 log.Debug("Creating reports")
259 for developerID, rt := range reviewTasksByDeveloper {
260 log.Debugf("Getting user with ID %d", developerID)
261 u, err := rm.User(developerID)
263 log.Fatalf(err.Error())
266 report.Developer = u.FirstName + " " + u.LastName + " <" + u.Mail + ">"
267 report.Email = u.Mail
268 report.SprintName = s.Name
269 report.SprintURL = conf.Endpoint + "/rb/taskboards/" + strconv.Itoa(s.ID)
270 report.SprintStartDate = s.StartDate
271 report.SprintDueDate = s.DueDate
272 report.UnassignedReviewTasks = UnassignedReviewTasks
273 for _, r := range rt {
274 log.Debugf("rt status %s", r.Status)
275 if r.Status == "In Progress" {
276 report.ReviewTasksInProgress = append(report.ReviewTasksInProgress, r)
277 } else if r.Status == "New" {
278 report.NewReviewTasks = append(report.NewReviewTasks, r)
281 if len(report.ReviewTasksInProgress) == 1 {
282 report.Subject = strings.Title(project) + ": you have 1 review to finish"
283 } else if len(report.ReviewTasksInProgress) > 0 {
284 report.Subject = strings.Title(project) + ": you have " + strconv.Itoa(len(report.ReviewTasksInProgress)) + " reviews to finish"
285 } else if len(report.UnassignedReviewTasks) > 0 {
286 report.Subject = strings.Title(project) + ": nobody is waiting on a review from you (but take an unassigned review, please?)"
288 report.Subject = strings.Title(project) + ": nobody is waiting on a review from you"
290 t, err := template.New("report").Parse(emailTemplate)
292 log.Fatalf(err.Error())
294 err = t.Execute(&report.Body, report)
296 log.Fatalf(err.Error())
299 log.Info("Sending e-mail report to " + report.Email)
300 _, err = report.SendEmail()
302 log.Fatalf(err.Error())
305 log.Info("Not sending e-mail (--send option not enabled), report follows:")
306 log.Info(report.Body.String())
308 log.Debug("Not sending e-mail (debug mode), report follows:")
309 log.Debug(report.Body.String())
316 func (r *Report) SendEmail() (bool, error) {
317 if err := smtp.SendMail("localhost:25", nil, "sysadmin@curii.com", []string{r.Email}, r.Body.Bytes()); err != nil {
323 func limit(text string, limit int) string {
324 if len(text) > limit {
325 return text[:limit] + "..."
332 if err := rootCmd.Execute(); err != nil {
333 fmt.Fprintln(os.Stderr, err)