package main import ( "flag" "log" "net/url" "regexp" "slices" "strconv" "time" "src.opensuse.org/autogits/common" "src.opensuse.org/autogits/common/gitea-generated/models" ) var configs common.AutogitConfigs var reviewRx *regexp.Regexp var rejectRx *regexp.Regexp var groupName string func InitRegex(groupName string) { reviewRx = regexp.MustCompile("^" + groupName + "\\s*:\\s*LGTM") rejectRx = regexp.MustCompile("^" + groupName + "\\s*:") } func ReviewAccepted(reviewText string) bool { return reviewRx.MatchString(reviewText) } func ReviewRejected(reviewText string) bool { return rejectRx.MatchString(reviewText) } func ProcessNotifications(notification *models.NotificationThread, gitea common.Gitea) { defer func() { if r := recover(); r != nil { log.Println("--- resovered") } }() rx := regexp.MustCompile(`^https://src\.(?:open)?suse\.(?:org|de)/api/v\d+/repos/(?[a-zA-Z0-9]+)/(?[_a-zA-Z0-9]+)/issues/(?[0-9]+)$`) subject := notification.Subject match := rx.FindStringSubmatch(subject.URL) if match == nil { log.Panicf("** Unexpected format of notification: %s", subject.URL) } org := match[1] repo := match[2] id, _ := strconv.ParseInt(match[3], 10, 64) log.Printf("processing: %s/%s#%d\n", org, repo, id) pr, err := gitea.GetPullRequest(org, repo, id) if err != nil { log.Println(" ** Cannot fetch PR associated with review:", subject.URL, "Error:", err) return } config := configs.GetPrjGitConfig(org, repo, pr.Base.Name) if pr.State == "closed" { // dismiss the review log.Println(" -- closed request, so nothing to review") gitea.SetNotificationRead(notification.ID) return } reviews, err := gitea.GetPullRequestReviews(org, repo, id) if err != nil { log.Println(" ** No reviews associated with request:", subject.URL, "Error:", err) return } requestReviewers, err := config.GetReviewGroupMembers(groupName) if err != nil { log.Println(err) return } // submitter cannot be reviewer requestReviewers = slices.DeleteFunc(requestReviewers, func(u string) bool { return u == pr.User.UserName }) for _, review := range reviews { user := review.User.UserName if !review.Stale && !review.Dismissed && slices.Contains(requestReviewers, user) { if review.State == common.ReviewStateApproved && ReviewAccepted(review.Body) { gitea.AddReviewComment(pr, common.ReviewStateApproved, "Signed off by: "+user) if err := gitea.SetNotificationRead(notification.ID); err != nil { log.Println(" Cannot set notification as read", err) } log.Println(" -> approved by", user) return } else if review.State == common.ReviewStateRequestChanges && ReviewRejected(review.Body) { gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Request changes. See review by: "+user) if err := gitea.SetNotificationRead(notification.ID); err != nil { log.Println(" Cannot set notification as read", err) } log.Println(" -> declined by", user) return } } } // request group member reviews, if missing log.Println(" Requesting reviews for:", requestReviewers) if _, err := gitea.RequestReviews(pr, requestReviewers...); err != nil { log.Println(" -> err:", err) } } func PeriodReviewCheck(gitea common.Gitea) { notifications, err := gitea.GetPullNotifications(nil) if err != nil { log.Println(" EEE Error fetching unread notifications: %w", err) return } for _, notification := range notifications { ProcessNotifications(notification, gitea) } } func main() { giteaUrl := flag.String("gitea-url", "https://src.opensuse.org", "Gitea instance used for reviews") rabbitMqHost := flag.String("rabbit-host", "rabbit.opensuse.org", "RabbitMQ instance where Gitea webhook notifications are sent") interval := flag.Int64("interval", 5, "Notification polling interval in minutes (min 1 min)") configFile := flag.String("config", "", "PrjGit listing config file") flag.Parse() args := flag.Args() if len(args) != 1 { log.Println(" syntax:") log.Println(" group-review [OPTIONS] ") log.Println() flag.Usage() return } groupName = args[0] configData, err := common.ReadConfigFile(*configFile) if err != nil { log.Println("Failed to read config file", err) return } if err := common.RequireGiteaSecretToken(); err != nil { log.Panicln(err) } if err := common.RequireRabbitSecrets(); err != nil { log.Panicln(err) } gitea := common.AllocateGiteaTransport(*giteaUrl) configs, err = common.ResolveWorkflowConfigs(gitea, configData) if err != nil { log.Panicln(err) } reviewer, err := gitea.GetCurrentUser() if err != nil { log.Panicln("Cannot fetch review user: %w", err) } if *interval < 1 { *interval = 1 } InitRegex(groupName) log.Println(" ** processing group reviews for group:", groupName) log.Println(" ** username in Gitea:", reviewer.UserName) log.Println(" ** polling internval:", *interval, "min") log.Println(" ** connecting to RabbitMQ:", *rabbitMqHost) if groupName != reviewer.UserName { log.Println(" ***** Reviewer does not match group name. Aborting. *****") return } _, err = url.Parse("amqps://" + *rabbitMqHost) if err != nil { log.Panicln("Cannot parse RabbitMQ host:", err) } /* common.ListenDefinitions{ RabbitURL: u, GitAuthor: groupName, Orgs: []string{"#"}, Handlers: map[string]common.RequestProcessor{ common.RequestType_PRReviewRequest: ReviewRequest, common.RequestType_PRReviewRejected: ProcessReview, common.RequestType_PRReviewAccepted: ProcessReview, }, } */ for { PeriodReviewCheck(gitea) time.Sleep(time.Duration(*interval * int64(time.Minute))) } }