package main import ( "flag" "fmt" "log" "net/url" "os" "regexp" "runtime/debug" "strconv" "strings" "time" "src.opensuse.org/autogits/common" "src.opensuse.org/autogits/common/gitea-generated/models" ) var LastDevelProjectUpdate *time.Time var Git common.GitHandlerGenerator func DevelProjectForPR(pr *models.PullRequest) (*common.DevelProject, error) { devels, err := common.FetchDevelProjects() if err != nil { common.LogError("Failed to fetch devel projects:", err) return nil, err } org := pr.Head.Repo.Owner.UserName pkg := pr.Head.Repo.Name common.LogDebug("Looking for devel package", org, pkg) for _, devel_project := range devels { if devel_project.Package == pkg { common.LogDebug("Fetching prject meta for", devel_project.Project) meta, err := Obs.GetProjectMeta(devel_project.Project) if err != nil { common.LogError("Failed to fetch devel project OBS meta", err) return nil, err } u, err := url.Parse(meta.ScmSync) if err != nil { common.LogError("Failed to parse project scm", err) return nil, err } if u.Hostname() != "src.opensuse.org" || strings.TrimSuffix(u.Path[1:], ".git") != org+"/_ObsPrj" { common.LogError("Invalid ScmSync format for devel project", meta.ScmSync, "Expected:", u.Path, "!=", org+"/_ObsPrj") return nil, fmt.Errorf("Invalid ScmSync format for devel project %s", meta.ScmSync) } g, err := Git.CreateGitHandler(org) if err != nil { common.LogError("Failed to alloate git:", err) return nil, err } defer g.Close() branch := u.Fragment u.Fragment = "" _, err = g.GitClone(common.DefaultGitPrj, branch, u.String()) common.PanicOnError(err) expectedSha, ok := g.GitSubmoduleCommitId(common.DefaultGitPrj, pkg, branch) if !ok { common.LogError("Failed to find", pkg, "in projectgit") return nil, fmt.Errorf("failed to find %s in projectgit", pkg) } if expectedSha == pr.Head.Sha { // found a match back to the devel project return devel_project, nil } return nil, fmt.Errorf("Failed to match submission to devel project") } } return nil, fmt.Errorf("Failed to find PR in a devel project. Ignoring") } func ProcessNotification(notification *models.NotificationThread) { defer func() { if r := recover(); r != nil { common.LogInfo("panic cought --- recovered") common.LogError(string(debug.Stack())) } }() rx := regexp.MustCompile(`^/?api/v\d+/repos/(?[_a-zA-Z0-9-]+)/(?[_a-zA-Z0-9-]+)/(?:issues|pulls)/(?[0-9]+)$`) subject := notification.Subject u, err := url.Parse(notification.Subject.URL) if err != nil { common.LogError("Invalid format of notification:", subject.URL, err) return } match := rx.FindStringSubmatch(u.Path) if match == nil { common.LogError("** Unexpected format of notification:", subject.URL) return } org := match[1] repo := match[2] id, _ := strconv.ParseInt(match[3], 10, 64) common.LogInfo("processing:", fmt.Sprintf("%s/%s!%d", org, repo, id)) pr, err := Gitea.GetPullRequest(org, repo, id) if err != nil { common.LogError(" ** Cannot fetch PR associated with review:", subject.URL, "Error:", err) return } repository := notification.Repository repoorg := repository.Owner.UserName reponame := repository.Name if repoorg != org || reponame != repo { common.LogError(" *** failed to parse org notification. Expected", repoorg, reponame) return } headSha := pr.Head.Sha timeline, err := common.FetchTimelineSinceLastPush(Gitea, headSha, org, repo, id) if err != nil { common.LogError("Failed to fetch comments:", err) return } ObsSrFormat := "OBS SR#%d\n" ExtractSR := func(body string) int { rx := regexp.MustCompile("^OBS SR#(\\d+)$") for _, line := range common.SplitLines(body) { if m := rx.FindStringSubmatch(line); m != nil && len(m) == 2 { id, _ := strconv.ParseInt(m[1], 10, 32) return int(id) } } return 0 } common.LogDebug("notification", org, repo, id) superseed := false for _, timeline := range timeline { if timeline.Type == common.TimelineCommentType_Comment && timeline.User.UserName == GiteaUser { // check if SR comment referenced here if sr := ExtractSR(timeline.Body); sr > 0 { status, err := Obs.RequestStatus(sr) if err != nil { common.LogError("Failed to request OBS request status", err) return } if superseed { break } common.LogInfo("Found status:", status.State.State) if !common.IsDryRun { if status.State.State == common.RequestStatus_Accepted { if _, err := Gitea.AddReviewComment(pr, common.ReviewStateApproved, "SR was accepted in OBS. Approving."); err != nil { common.LogError("Failed to add review comment to PR:", err) return } } else if status.State.State == common.RequestStatus_Declined || status.State.State == common.RequestStatus_Revoked { if _, err := Gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "SR was rejected in OBS. Rejecting."); err != nil { common.LogError("Failed to add review comment to PR:", err) return } } else { common.LogDebug("Request is in state:", status.State.State, "Waiting.") return } Gitea.SetNotificationRead(notification.ID) } else { } return } } else if timeline.Type == common.TimelineCommentType_PushPull { superseed = true } } // no current SR running, create one dp, err := DevelProjectForPR(pr) if err != nil { common.LogDebug("Failed to process PR:", err) return } if !common.IsDryRun { meta, err := Obs.CreateSubmitRequest(dp.Project, dp.Package, ObsTarget) if err != nil { common.LogError("Failed to create OBS SR: ", dp.Project, dp.Package, "=>", ObsTarget, err) return } for { // make sure we leave comment here err = Gitea.AddComment(pr, "Created OBS submit request to "+ObsTarget+"\n\n"+fmt.Sprintf(ObsSrFormat, meta.Id)) if err == nil { break } common.LogError("Failed to create Gitea comment:", err) common.LogInfo("Waiting 1 minute and retrying to leave comment...") time.Sleep(time.Minute) } } else { common.LogInfo("Would create a SR from", dp.Project, "/", dp.Package, "=>", ObsTarget) } } func ProcessNotifications() { // process PRs and issues notifications, err := Gitea.GetNotifications(common.GiteaNotificationType_Pull, nil) if err != nil { common.LogError("Failed to get notifications.", err) return } for _, notification := range notifications { ProcessNotification(notification) } } var GiteaUser string var Gitea common.Gitea var Obs *common.ObsClient var ObsTarget, GiteaTargetBranch, GiteaOrg string func main() { GiteaHost := flag.String("gitea-host", "https://src.opensuse.org", "Gitea host") ObsHost := flag.String("obs-host", "https://api.opensuse.org", "OBS instance") flag.StringVar(&ObsTarget, "obs-target", "openSUSE:Factory", "") flag.StringVar(&GiteaTargetBranch, "gitea-target", "factory", "") flag.StringVar(&GiteaOrg, "gitea-org", "pool", "") debug := flag.Bool("debug", false, "Debug logging") GitRepoPath := flag.String("git-path", "", "Git repo path") flag.BoolVar(&common.IsDryRun, "dry", false, "no-op operation") flag.Parse() if *debug { common.SetLoggingLevel(common.LogLevelDebug) } var err error if err = common.RequireGiteaSecretToken(); err != nil { log.Panic(err) } if err = common.RequireObsSecretToken(); err != nil { log.Panic(err) } if Obs, err = common.NewObsClient(*ObsHost); err != nil { log.Panic(err) } Gitea = common.AllocateGiteaTransport(*GiteaHost) if user, err := Gitea.GetCurrentUser(); err != nil { log.Panic(err) } else { GiteaUser = user.UserName } common.LogInfo("Current user:", GiteaUser) if len(*GitRepoPath) == 0 { *GitRepoPath, err = os.MkdirTemp(os.TempDir(), "forward-bot") if err != nil { common.LogError("Failed to create tempdir:", err) return } } Git, err = common.AllocateGitWorkTree(*GitRepoPath, "bot", "nothing") if err != nil { common.LogError("Failed to allocate git tree", err) return } for { common.LogDebug("--- Starting processing notifications ---") ProcessNotifications() common.LogDebug("--- End processing notifications ---") time.Sleep(time.Minute * 5) } }