package main import ( "fmt" "log" "net/url" "os" "path" "regexp" "slices" "strconv" "strings" "time" "src.opensuse.org/autogits/common" "src.opensuse.org/autogits/common/gitea-generated/models" ) const ( GitAuthor = "GiteaBot - Obs Staging" BotName = "ObsStaging" ObsBuildBot = "/obsbuild" Username = "autogits_obs_staging_bot" ) var GiteaToken string var runId uint func failOnError(err error, msg string) { if err != nil { log.Panicf("%s: %s", err, msg) } } func fetchPrGit(h *common.RequestHandler, pr *models.PullRequest) error { // clone PR head and base and return path if h.HasError() { return h.Error } if _, err := os.Stat(path.Join(h.GitPath, pr.Head.Sha)); os.IsNotExist(err) { h.GitExec("", "clone", "--depth", "1", pr.Head.Repo.CloneURL, pr.Head.Sha) h.GitExec(pr.Head.Sha, "fetch", "--depth", "1", "origin", pr.Head.Sha, pr.Base.Sha) } else if err != nil { h.Error = err } return h.Error } func getObsProjectAssociatedWithPr(baseProject string, pr *models.PullRequest) string { return fmt.Sprintf( "%s:%s:%s:PR:%d", baseProject, common.ObsSafeProjectName(pr.Base.Repo.Owner.UserName), common.ObsSafeProjectName(pr.Base.Repo.Name), pr.Index, ) } type BuildStatusSummary string const BuildUnresolveable = BuildStatusSummary("unresolveable") const BuildBuilding = BuildStatusSummary("building") const BuildFailed = BuildStatusSummary("failed") const BuildSuccess = BuildStatusSummary("success") type BuildStatus struct { Status BuildStatusSummary Detail string } var buildStatus map[string]BuildStatus func processBuildStatusUpdate() { } func processBuildStatus(project, refProject *common.BuildResultList) BuildStatusSummary { return BuildBuilding } func startBuild(h *common.RequestHandler, pr *models.PullRequest, obsClient *common.ObsClient) error { err := fetchPrGit(h, pr) if err != nil { h.LogError("Cannot fetch PR git: %s", pr.URL) return err } // find modified submodules and new submodules -- build them dir := pr.Head.Sha headSubmodules := h.GitSubmoduleList(dir, pr.Head.Sha) baseSubmodules := h.GitSubmoduleList(dir, pr.Base.Sha) modifiedOrNew := make([]string, 0, 16) for pkg, headOid := range headSubmodules { if baseOid, exists := baseSubmodules[pkg]; !exists || baseOid != headOid { modifiedOrNew = append(modifiedOrNew, pkg) } } h.Log("repo content fetching ...") buildPrjBytes := h.GitCatFile(dir, pr.Head.Sha, "project.build") // buildPrjBytes, err := h.GetPullRequestFileContent(pr, "project.build") if h.HasError() { h.LogPlainError(h.Error) /* _, err := h.AddReviewComment(pr, common.ReviewStateRequestChanges, "Cannot find reference project") if err != nil { h.LogPlainError(err) } */ return h.Error } buildPrj := strings.TrimSpace(string(buildPrjBytes)) meta, err := obsClient.GetProjectMeta(buildPrj) if err != nil { h.Log("error fetching project meta for %s: %v", buildPrj, err) return err } // generate new project with paths pointinig back to original repos // disable publishing // TODO: escape things here meta.Name = getObsProjectAssociatedWithPr(obsClient.HomeProject, pr) meta.Description = fmt.Sprintf(`Pull request build job: %s%s PR#%d`, "https://src.opensuse.org", pr.Base.Repo.Name, pr.Index) urlPkg := make([]string, 0, len(modifiedOrNew)) for _, pkg := range modifiedOrNew { urlPkg = append(urlPkg, "onlybuild="+url.QueryEscape(pkg)) } meta.ScmSync = pr.Head.Repo.CloneURL + "?" + strings.Join(urlPkg, "&") meta.Title = fmt.Sprintf("PR#%d to %s", pr.Index, pr.Base.Name) meta.PublicFlags = common.Flags{Contents: ""} // set paths to parent project for idx, r := range meta.Repositories { meta.Repositories[idx].Paths = []common.RepositoryPathMeta{{ Project: buildPrj, Repository: r.Name, }} } h.Log("%#v", meta) err = obsClient.SetProjectMeta(meta) if err != nil { h.Error = err h.LogError("cannot create meta project: %#v", err) return h.Error } return nil } func processPullNotification(h *common.RequestHandler, thread *models.NotificationThread) { rx := regexp.MustCompile(`^https://src\.(?:open)?suse\.(?:org|de)/api/v\d+/repos/(?[a-zA-Z0-9]+)/(?[_a-zA-Z0-9]+)/issues/(?[0-9]+)$`) notification := thread.Subject match := rx.FindStringSubmatch(notification.URL) if match == nil { log.Panicf("Unexpected format of notification: %s", notification.URL) } h.Log("processing") h.Log("project: %s", match[2]) h.Log("org: %s", match[1]) h.Log("number: %s", match[3]) org := match[1] repo := match[2] id, _ := strconv.ParseInt(match[3], 10, 64) pr, reviews, err := h.GetPullRequestAndReviews(org, repo, id) if err != nil { return } obsClient, err := common.NewObsClient("api.opensuse.org") if err != nil { h.LogPlainError(err) return } reviewRequested := false for _, reviewer := range pr.RequestedReviewers { if reviewer.UserName == Username { reviewRequested = true break } } if !reviewRequested { return } newReviews := make([]*models.PullReview, 0, len(reviews)) for _, review := range reviews { if review.User.UserName == Username { newReviews = append(newReviews, review) } } reviews = newReviews slices.SortFunc(reviews, func(a, b *models.PullReview) int { return time.Time(a.Submitted).Compare(time.Time(b.Submitted)) }) for idx := len(reviews) - 1; idx >= 0; idx-- { review := reviews[idx] h.Log("state: %s, body: %s, id:%d\n", string(review.State), review.Body, review.ID) if review.User.UserName != "autogits_obs_staging_bot" { continue } h.Log("processing state...") switch review.State { // create build project, if doesn't exist, and add it to pending requests case common.ReviewStateUnknown, common.ReviewStateRequestReview: if err := startBuild(h, pr, obsClient); err != nil { return } msg := "Build is started in https://build.opensuse.org/project/show/" + getObsProjectAssociatedWithPr(obsClient.HomeProject, pr) h.AddReviewComment(pr, common.ReviewStatePending, msg) case common.ReviewStatePending: err := fetchPrGit(h, pr) if err != nil { h.LogError("Cannot fetch PR git: %s", pr.URL) return } // find modified submodules and new submodules -- build them dir := pr.Head.Sha headSubmodules := h.GitSubmoduleList(dir, pr.Head.Sha) baseSubmodules := h.GitSubmoduleList(dir, pr.Base.Sha) modifiedOrNew := make([]string, 0, 16) for pkg, headOid := range headSubmodules { if baseOid, exists := baseSubmodules[pkg]; !exists || baseOid != headOid { modifiedOrNew = append(modifiedOrNew, pkg) } } h.Log("repo content fetching ...") refProject := string(h.GitCatFile(dir, pr.Head.Sha, "project.build")) // buildPrjBytes, err := h.GetPullRequestFileContent(pr, "project.build") if h.HasError() { h.LogPlainError(h.Error) /* _, err := h.AddReviewComment(pr, common.ReviewStateRequestChanges, "Cannot find reference project") if err != nil { h.LogPlainError(err) } */ return } obsProject := getObsProjectAssociatedWithPr(obsClient.HomeProject, pr) prjResult, err := obsClient.BuildStatus(obsProject) if err != nil { h.LogError("failed fetching build status for '%s': %v", obsProject, err) return } refProjectResult, err := obsClient.BuildStatus(refProject) if err != nil { h.LogError("failed fetching ref project status for '%s': %v", refProject, err) } buildStatus := processBuildStatus(prjResult, refProjectResult) switch buildStatus { case BuildSuccess: _, err := h.AddReviewComment(pr, common.ReviewStateApproved, "Build successful") if err != nil { h.LogPlainError(err) } case BuildFailed: _, err := h.AddReviewComment(pr, common.ReviewStateRequestChanges, "Build failed") if err != nil { h.LogPlainError(err) } case BuildBuilding: } // waiting for build results // project := getObsProjectAssociatedWithPr(obsClient.HomeProject, pr) case common.ReviewStateApproved: // done, mark notification as read h.Log("processing request for success build ...") // h.SetNotificationRead(thread.ID) case common.ReviewStateRequestChanges: // build failures, nothing to do here, mark notification as read h.Log("processing request for failed request changes...") // h.SetNotificationRead(thread.ID) } break } } func pollWorkNotifications() { h := common.CreateRequestHandler(GitAuthor, BotName) data, err := h.GetNotifications(nil) if err != nil { h.LogPlainError(err) return } if data != nil { for _, notification := range data { switch notification.Subject.Type { case "Pull": processPullNotification(h, notification) default: h.SetNotificationRead(notification.ID) } } } } func main() { failOnError(common.RequireGiteaSecretToken(), "Cannot find GITEA_TOKEN") failOnError(common.RequireObsSecretToken(), "Cannot find OBS_USER and OBS_PASSWORD") // go ProcessingObsMessages("rabbit.opensuse.org", "opensuse", "opensuse", "") // for { pollWorkNotifications() // time.Sleep(1000) // } stuck := make(chan int) <-stuck }