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 now := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)
157 // Find any current sprint(s)
158 for _, v := range versions {
160 if v.Status != "open" {
163 // The due date must be in the future
167 dueDate, err := time.Parse("2006-01-02", v.DueDate)
169 log.Fatalf(err.Error())
171 if dueDate.Before(now) {
174 // The start date must be in the past (have to look up the Sprint object!)
175 log.Debugf("Getting sprint with id %d", v.ID)
176 s, err := rm.Sprint(v.ID)
178 log.Fatalf(err.Error())
180 if s.StartDate == "" {
183 startDate, err := time.Parse("2006-01-02", s.StartDate)
185 log.Fatalf(err.Error())
187 if startDate.After(now) {
190 // Found a current sprint
191 log.Debugf("Current sprint: %+#v", s)
193 // Get the issues from this sprint
194 var issueFilter redmine.IssueFilter
195 issueFilter.VersionID = strconv.Itoa(v.ID)
196 log.Debugf("Getting issues with version ID %d", v.ID)
197 issues, err := rm.FilteredIssues(&issueFilter)
199 log.Fatalf(err.Error())
202 log.Debugf("Retrieved %d issues", len(issues))
203 reviewTasksByDeveloper := make(map[int][]ReviewTask)
204 var UnassignedReviewTasks []ReviewTask
205 for _, i := range issues {
206 log.Debugf("Considering issue (%s): %+#v, \"%s\"", i.Tracker.Name, i.ID, i.Subject)
208 if i.Tracker.Name != "Task" {
211 // Filter for review tasks (issue subject must start with 'review')
212 reviewRE := regexp.MustCompile(`^(Review|review)`)
213 if !reviewRE.MatchString(i.Subject) {
216 // Is the task assigned?
217 if i.AssignedTo == nil {
218 log.Debugf("Found unassigned review task: %+#v, \"%s\"", i.ID, i.Subject)
219 log.Debugf("Getting parent issue with ID %d", i.Parent.ID)
220 parent, err := rm.GetIssue(i.Parent.ID)
222 log.Fatalf(err.Error())
225 IssueID: "#" + strconv.Itoa(parent.ID),
226 IssueSubject: limit(parent.Subject, 58),
227 IssueURL: conf.Endpoint + "/issues/" + strconv.Itoa(parent.ID),
228 ID: "#" + strconv.Itoa(i.ID),
229 Subject: limit(i.Subject, 58),
230 URL: conf.Endpoint + "/issues/" + strconv.Itoa(i.ID),
231 Status: i.Status.Name,
233 UnassignedReviewTasks = append(UnassignedReviewTasks, rt)
236 // Found an assigned review task
237 log.Debugf("Found assigned review task: %+#v, \"%s\", assigned to %+#v", i.ID, i.Subject, i.AssignedTo.ID)
238 if _, ok := reviewTasksByDeveloper[i.AssignedTo.ID]; !ok {
239 reviewTasksByDeveloper[i.AssignedTo.ID] = []ReviewTask{}
241 log.Debugf("Getting parent issue with ID %d", i.Parent.ID)
242 parent, err := rm.GetIssue(i.Parent.ID)
244 log.Fatalf(err.Error())
247 IssueID: "#" + strconv.Itoa(parent.ID),
248 IssueSubject: limit(parent.Subject, 58),
249 IssueURL: conf.Endpoint + "/issues/" + strconv.Itoa(parent.ID),
250 ID: "#" + strconv.Itoa(i.ID),
251 Subject: limit(i.Subject, 58),
252 URL: conf.Endpoint + "/issues/" + strconv.Itoa(i.ID),
253 Status: i.Status.Name,
255 reviewTasksByDeveloper[i.AssignedTo.ID] = append(reviewTasksByDeveloper[i.AssignedTo.ID], rt)
258 // Create the report(s)
259 log.Debug("Creating reports")
260 for developerID, rt := range reviewTasksByDeveloper {
261 log.Debugf("Getting user with ID %d", developerID)
262 u, err := rm.User(developerID)
264 log.Fatalf(err.Error())
267 report.Developer = u.FirstName + " " + u.LastName + " <" + u.Mail + ">"
268 report.Email = u.Mail
269 report.SprintName = s.Name
270 report.SprintURL = conf.Endpoint + "/rb/taskboards/" + strconv.Itoa(s.ID)
271 report.SprintStartDate = s.StartDate
272 report.SprintDueDate = s.DueDate
273 report.UnassignedReviewTasks = UnassignedReviewTasks
274 for _, r := range rt {
275 log.Debugf("rt status %s", r.Status)
276 if r.Status == "In Progress" {
277 report.ReviewTasksInProgress = append(report.ReviewTasksInProgress, r)
278 } else if r.Status == "New" {
279 report.NewReviewTasks = append(report.NewReviewTasks, r)
282 if len(report.ReviewTasksInProgress) == 1 {
283 report.Subject = strings.Title(project) + ": you have 1 review to finish"
284 } else if len(report.ReviewTasksInProgress) > 0 {
285 report.Subject = strings.Title(project) + ": you have " + strconv.Itoa(len(report.ReviewTasksInProgress)) + " reviews to finish"
286 } else if len(report.UnassignedReviewTasks) > 0 {
287 report.Subject = strings.Title(project) + ": nobody is waiting on a review from you (but take an unassigned review, please?)"
289 report.Subject = strings.Title(project) + ": nobody is waiting on a review from you"
291 t, err := template.New("report").Parse(emailTemplate)
293 log.Fatalf(err.Error())
295 err = t.Execute(&report.Body, report)
297 log.Fatalf(err.Error())
300 log.Info("Sending e-mail report to " + report.Email)
301 _, err = report.SendEmail()
303 log.Fatalf(err.Error())
306 log.Info("Not sending e-mail (--send option not enabled), report follows:")
307 log.Info(report.Body.String())
309 log.Debug("Not sending e-mail (debug mode), report follows:")
310 log.Debug(report.Body.String())
317 func (r *Report) SendEmail() (bool, error) {
318 if err := smtp.SendMail("localhost:25", nil, "sysadmin@curii.com", []string{r.Email}, r.Body.Bytes()); err != nil {
324 func limit(text string, limit int) string {
325 if len(text) > limit {
326 return text[:limit] + "..."
333 if err := rootCmd.Execute(); err != nil {
334 fmt.Fprintln(os.Stderr, err)