autogits/obs-staging-bot/main.go

551 lines
15 KiB
Go
Raw Normal View History

2024-07-07 21:08:41 +02:00
package main
2024-09-10 18:24:41 +02:00
/*
* This file is part of Autogits.
*
* Copyright © 2024 SUSE LLC
*
* Autogits is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 2 of the License, or (at your option) any later
* version.
*
* Autogits is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* Foobar. If not, see <https://www.gnu.org/licenses/>.
*/
2024-07-07 21:08:41 +02:00
import (
2024-08-01 12:36:23 +02:00
"bytes"
"errors"
2024-07-26 16:53:09 +02:00
"fmt"
2024-07-18 23:36:41 +02:00
"log"
2024-07-27 20:57:18 +02:00
"net/url"
2024-07-18 17:26:17 +02:00
"os"
2024-07-26 16:53:09 +02:00
"path"
2024-07-18 23:36:41 +02:00
"regexp"
2024-07-27 20:57:18 +02:00
"slices"
2024-07-18 23:36:41 +02:00
"strconv"
2024-07-26 16:53:09 +02:00
"strings"
2024-07-27 20:57:18 +02:00
"time"
2024-07-16 22:05:44 +02:00
2024-07-07 21:08:41 +02:00
"src.opensuse.org/autogits/common"
2024-07-18 23:36:41 +02:00
"src.opensuse.org/autogits/common/gitea-generated/models"
2024-07-07 21:08:41 +02:00
)
const (
2024-07-18 23:36:41 +02:00
GitAuthor = "GiteaBot - Obs Staging"
2024-07-26 16:53:09 +02:00
BotName = "ObsStaging"
2024-07-16 17:05:43 +02:00
ObsBuildBot = "/obsbuild"
2024-07-28 21:25:44 +02:00
Username = "autogits_obs_staging_bot"
2024-07-07 21:08:41 +02:00
)
var GiteaToken string
2024-07-18 23:36:41 +02:00
var runId uint
2024-07-07 21:08:41 +02:00
2024-07-18 23:36:41 +02:00
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", err, msg)
}
}
2024-07-16 22:05:44 +02:00
2024-07-26 16:53:09 +02:00
func fetchPrGit(h *common.RequestHandler, pr *models.PullRequest) error {
// clone PR head and base and return path
2024-07-31 16:52:02 +02:00
if h.HasError() {
return h.Error
}
2024-07-26 16:53:09 +02:00
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
2024-07-16 22:05:44 +02:00
}
2024-07-26 16:53:09 +02:00
return h.Error
2024-07-16 22:05:44 +02:00
}
2024-07-07 21:08:41 +02:00
2024-07-29 15:28:03 +02:00
func getObsProjectAssociatedWithPr(baseProject string, pr *models.PullRequest) string {
2024-08-19 17:14:20 +02:00
if pr.Base.Repo.Name == common.DefaultGitPrj {
// if default project git, we don't need it in project name to be unique
return fmt.Sprintf(
"%s:%s:PR:%d",
baseProject,
common.ObsSafeProjectName(pr.Base.Repo.Owner.UserName),
pr.Index,
)
}
2024-07-29 15:28:03 +02:00
return fmt.Sprintf(
"%s:%s:%s:PR:%d",
baseProject,
common.ObsSafeProjectName(pr.Base.Repo.Owner.UserName),
common.ObsSafeProjectName(pr.Base.Repo.Name),
pr.Index,
)
}
2024-08-01 18:10:45 +02:00
func processBuildStatusUpdate() {
2024-07-31 16:52:02 +02:00
}
2024-08-01 18:10:45 +02:00
type BuildStatusSummary int
2024-07-31 16:52:02 +02:00
2024-08-01 18:10:45 +02:00
const (
BuildStatusSummarySuccess = 1
BuildStatusSummaryFailed = 2
BuildStatusSummaryBuilding = 3
BuildStatusSummaryUnknown = 4
)
func processBuildStatus(h *common.RequestHandler, project, refProject *common.BuildResultList) BuildStatusSummary {
if _, finished := project.BuildResultSummary(); !finished {
return BuildStatusSummaryBuilding
}
if _, finished := refProject.BuildResultSummary(); !finished {
h.LogError("refProject not finished building??")
return BuildStatusSummaryUnknown
}
// the repositories should be setup equally between the projects. We
// need to verify that packages that are building in `refProject` are not
// failing in the `project`
BuildResultSorter := func(a, b common.BuildResult) int {
if c := strings.Compare(a.Repository, b.Repository); c != 0 {
return c
}
if c := strings.Compare(a.Arch, b.Arch); c != 0 {
return c
}
panic("Should not happen -- BuiltResultSorter equal repos?")
}
slices.SortFunc(project.Result, BuildResultSorter)
2024-08-02 15:34:44 +02:00
if refProject == nil {
// just return if buid finished and have some successes, since new package
SomeSuccess := false
for i := 0; i < len(project.Result); i++ {
repoRes := &project.Result[i]
repoResStatus, ok := common.ObsRepoStatusDetails[repoRes.Code]
if !ok {
2024-08-16 14:23:04 +02:00
h.LogError("cannot find code: %s", repoRes.Code)
2024-08-02 15:34:44 +02:00
return BuildStatusSummaryUnknown
}
if !repoResStatus.Finished {
return BuildStatusSummaryBuilding
}
for _, pkg := range repoRes.Status {
pkgStatus, ok := common.ObsBuildStatusDetails[pkg.Code]
if !ok {
h.LogError("Unknown package build status: %s for %s", pkg.Code, pkg.Package)
h.LogError("Details: %s", pkg.Details)
return BuildStatusSummaryUnknown
}
if pkgStatus.Success {
SomeSuccess = true
}
}
}
if SomeSuccess {
return BuildStatusSummarySuccess
}
return BuildStatusSummaryFailed
}
2024-08-01 18:10:45 +02:00
slices.SortFunc(refProject.Result, BuildResultSorter)
2024-07-31 16:52:02 +02:00
2024-08-16 14:32:32 +02:00
h.Log("comparing results %d vs. ref %d", len(project.Result), len(refProject.Result))
2024-08-02 14:42:04 +02:00
for i := 0; i < len(project.Result); i++ {
2024-08-16 14:32:32 +02:00
h.Log("searching for %s/%s", project.Result[i].Repository, project.Result[i].Arch)
2024-08-16 14:34:42 +02:00
j := 0
2024-08-19 13:54:44 +02:00
found:
2024-08-02 14:42:04 +02:00
for ; j < len(refProject.Result); j++ {
if project.Result[i].Repository != refProject.Result[j].Repository ||
project.Result[i].Arch != refProject.Result[j].Arch {
continue
}
2024-08-16 14:37:19 +02:00
h.Log("found match for %s/%s @ %d", project.Result[i].Repository, project.Result[i].Arch, j)
2024-08-02 14:42:04 +02:00
res := processRepoBuildStatus(h, project.Result[i].Status, refProject.Result[j].Status)
switch res {
case BuildStatusSummarySuccess:
2024-08-16 14:39:20 +02:00
break found
2024-08-02 14:42:04 +02:00
default:
return res
}
}
2024-08-16 14:34:42 +02:00
h.Log("%d", j)
2024-08-02 14:42:04 +02:00
if j >= len(refProject.Result) {
2024-08-16 14:23:04 +02:00
h.LogError("Cannot find results... %#v \n %#v\n", project.Result[i], refProject.Result)
2024-08-02 14:42:04 +02:00
return BuildStatusSummaryUnknown
}
}
return BuildStatusSummarySuccess
}
func processRepoBuildStatus(h *common.RequestHandler, results, ref []common.PackageBuildStatus) BuildStatusSummary {
PackageBuildStatusSorter := func(a, b common.PackageBuildStatus) int {
return strings.Compare(a.Package, b.Package)
}
// compare build result
slices.SortFunc(results, PackageBuildStatusSorter)
slices.SortFunc(ref, PackageBuildStatusSorter)
j := 0
for i := 0; i < len(results); i++ {
res, ok := common.ObsBuildStatusDetails[results[i].Code]
if !ok {
h.LogError("unknown package result code: %s for package %s", results[i].Code, results[i].Package)
return BuildStatusSummaryUnknown
}
if !res.Finished {
return BuildStatusSummaryBuilding
}
if !res.Success {
// not failed if reference project also failed for same package here
for ; j < len(results) && strings.Compare(results[i].Package, ref[j].Package) < 0; j++ {
}
if j < len(results) && results[i].Package == ref[j].Package {
refRes, ok := common.ObsBuildStatusDetails[ref[j].Code]
if !ok {
h.LogError("unknown package result code: %s for package %s", ref[j].Code, ref[j].Package)
return BuildStatusSummaryUnknown
}
if !refRes.Finished {
h.LogError("not finished building in reference project?!")
}
if refRes.Success {
return BuildStatusSummaryFailed
}
}
}
}
return BuildStatusSummarySuccess
2024-07-31 16:52:02 +02:00
}
2024-08-19 13:54:44 +02:00
func generateObsPrjMeta(h *common.RequestHandler, pr *models.PullRequest, obsClient *common.ObsClient) (*common.ProjectMeta, error) {
h.Log("repo content fetching ...")
2024-07-31 17:17:07 +02:00
err := fetchPrGit(h, pr)
if err != nil {
h.LogError("Cannot fetch PR git: %s", pr.URL)
2024-08-19 13:54:44 +02:00
return nil, err
2024-07-31 17:17:07 +02:00
}
// 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)
}
}
2024-08-01 12:36:23 +02:00
buildPrj := string(bytes.TrimSpace(h.GitCatFile(dir, pr.Head.Sha, "project.build")))
if len(buildPrj) < 1 {
_, err := h.AddReviewComment(pr, common.ReviewStateRequestChanges, "Cannot find reference project")
if err != nil {
h.LogPlainError(err)
2024-08-19 13:54:44 +02:00
return nil, h.Error
2024-08-01 12:36:23 +02:00
}
2024-08-19 13:54:44 +02:00
return nil, fmt.Errorf("Cannot find reference project for %s PR#%d", pr.Base.Name, pr.Index)
2024-08-01 12:36:23 +02:00
}
2024-07-31 17:17:07 +02:00
if h.HasError() {
h.LogPlainError(h.Error)
2024-08-19 13:54:44 +02:00
return nil, h.Error
2024-07-31 17:17:07 +02:00
}
meta, err := obsClient.GetProjectMeta(buildPrj)
if err != nil {
h.Log("error fetching project meta for %s: %v", buildPrj, err)
2024-08-19 13:54:44 +02:00
return nil, err
2024-07-31 17:17:07 +02:00
}
// generate new project with paths pointinig back to original repos
// disable publishing
meta.Name = getObsProjectAssociatedWithPr(obsClient.HomeProject, pr)
2024-08-01 12:36:23 +02:00
meta.Description = fmt.Sprintf(`Pull request build job PR#%d to branch %s of %s/%s`,
pr.Index, pr.Base.Name, pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name)
meta.Url = fmt.Sprintf(
"https://src.opensuse.org/%s/%s/pulls/%d",
url.PathEscape(pr.Base.Repo.Owner.UserName),
url.PathEscape(pr.Base.Repo.Name),
pr.Index,
)
2024-07-31 17:17:07 +02:00
urlPkg := make([]string, 0, len(modifiedOrNew))
for _, pkg := range modifiedOrNew {
urlPkg = append(urlPkg, "onlybuild="+url.QueryEscape(pkg))
}
2024-08-19 13:54:44 +02:00
meta.ScmSync = pr.Head.Repo.CloneURL + "?" + strings.Join(urlPkg, "&") + "#" + pr.Head.Sha
2024-07-31 17:17:07 +02:00
meta.Title = fmt.Sprintf("PR#%d to %s", pr.Index, pr.Base.Name)
meta.PublicFlags = common.Flags{Contents: "<disable/>"}
// 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)
2024-08-19 13:54:44 +02:00
return meta, nil
}
func startOrUpdateBuild(h *common.RequestHandler, pr *models.PullRequest, obsClient *common.ObsClient) error {
h.Log("fetching OBS project Meta")
obsPrProject := getObsProjectAssociatedWithPr(obsClient.HomeProject, pr)
meta, err := obsClient.GetProjectMeta(obsPrProject)
if err != nil {
h.Log("error fetching project meta for %s: %v", obsPrProject, err)
return err
}
if meta != nil {
path, err := url.Parse(meta.ScmSync)
if err != nil {
h.Log("Cannot parse SCMSYNC url: '%s' .. regenerating build", meta.ScmSync)
meta = nil
} else {
if path.Fragment == pr.Head.Sha {
// build in progress
return nil
}
// build needs update
h.Log("Detected Head update... regenerating build...")
meta = nil
}
}
if meta == nil {
// new build
meta, err = generateObsPrjMeta(h, pr, obsClient)
if err != nil {
return err
}
}
2024-07-31 17:17:07 +02:00
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) {
2024-07-26 16:53:09 +02:00
rx := regexp.MustCompile(`^https://src\.(?:open)?suse\.(?:org|de)/api/v\d+/repos/(?<org>[a-zA-Z0-9]+)/(?<project>[_a-zA-Z0-9]+)/issues/(?<num>[0-9]+)$`)
2024-07-31 17:17:07 +02:00
notification := thread.Subject
2024-07-18 23:36:41 +02:00
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 {
2024-08-05 17:00:39 +02:00
h.LogError("No PR associated with review: %s. Error: %v", notification.URL, err)
2024-07-18 23:36:41 +02:00
return
}
2024-07-22 17:09:45 +02:00
obsClient, err := common.NewObsClient("api.opensuse.org")
if err != nil {
h.LogPlainError(err)
return
}
2024-07-28 21:25:44 +02:00
reviewRequested := false
for _, reviewer := range pr.RequestedReviewers {
if reviewer.UserName == Username {
reviewRequested = true
break
}
}
if !reviewRequested {
2024-08-05 17:00:39 +02:00
h.Log("Review not requested in notification. Setting to status 'read'")
h.SetNotificationRead(thread.ID)
2024-07-28 21:25:44 +02:00
return
}
newReviews := make([]*models.PullReview, 0, len(reviews))
for _, review := range reviews {
if review.User.UserName == Username {
newReviews = append(newReviews, review)
}
}
reviews = newReviews
2024-07-27 20:57:18 +02:00
slices.SortFunc(reviews, func(a, b *models.PullReview) int {
return time.Time(a.Submitted).Compare(time.Time(b.Submitted))
})
2024-07-28 21:25:44 +02:00
for idx := len(reviews) - 1; idx >= 0; idx-- {
2024-07-27 20:57:18 +02:00
review := reviews[idx]
2024-07-18 23:36:41 +02:00
h.Log("state: %s, body: %s, id:%d\n", string(review.State), review.Body, review.ID)
2024-07-26 16:53:09 +02:00
if review.User.UserName != "autogits_obs_staging_bot" {
2024-07-18 23:36:41 +02:00
continue
}
2024-07-26 16:53:09 +02:00
h.Log("processing state...")
2024-07-18 23:36:41 +02:00
switch review.State {
2024-07-31 17:17:07 +02:00
2024-07-22 17:09:45 +02:00
// create build project, if doesn't exist, and add it to pending requests
2024-07-26 16:53:09 +02:00
case common.ReviewStateUnknown, common.ReviewStateRequestReview:
2024-08-19 13:54:44 +02:00
if err := startOrUpdateBuild(h, pr, obsClient); err != nil {
2024-07-29 15:28:03 +02:00
return
}
2024-07-31 17:17:07 +02:00
msg := "Build is started in https://build.opensuse.org/project/show/" +
getObsProjectAssociatedWithPr(obsClient.HomeProject, pr)
h.AddReviewComment(pr, common.ReviewStatePending, msg)
2024-07-27 20:57:18 +02:00
2024-07-18 23:36:41 +02:00
case common.ReviewStatePending:
2024-07-31 16:52:02 +02:00
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 ...")
2024-08-01 18:10:45 +02:00
refPrj := string(bytes.TrimSpace(h.GitCatFile(dir, pr.Head.Sha, "project.build")))
if len(refPrj) < 1 {
_, err := h.AddReviewComment(pr, common.ReviewStateRequestChanges, "Cannot find reference project")
if err != nil {
h.LogPlainError(err)
return
}
h.LogError("Cannot find reference project for %s PR#%d", pr.Base.Name, pr.Index)
return
}
2024-07-31 16:52:02 +02:00
if h.HasError() {
h.LogPlainError(h.Error)
return
}
obsProject := getObsProjectAssociatedWithPr(obsClient.HomeProject, pr)
prjResult, err := obsClient.BuildStatus(obsProject)
if err != nil {
2024-08-01 12:36:23 +02:00
if errors.Is(err, common.ObsProjectNotFound{Project: obsProject}) {
// recreate missing project
h.LogError("missing OBS project ... recreating '%s': %v", obsProject, err)
2024-08-19 13:54:44 +02:00
startOrUpdateBuild(h, pr, obsClient)
2024-08-01 18:10:45 +02:00
2024-08-01 12:36:23 +02:00
return
}
2024-07-31 16:52:02 +02:00
h.LogError("failed fetching build status for '%s': %v", obsProject, err)
return
}
2024-08-16 14:04:43 +02:00
2024-08-01 18:10:45 +02:00
refProjectResult, err := obsClient.BuildStatus(refPrj, prjResult.GetPackageList()...)
2024-07-31 16:52:02 +02:00
if err != nil {
2024-08-01 18:10:45 +02:00
h.LogError("failed fetching ref project status for '%s': %v", refPrj, err)
2024-07-31 16:52:02 +02:00
}
2024-08-01 18:10:45 +02:00
buildStatus := processBuildStatus(h, prjResult, refProjectResult)
2024-07-31 16:52:02 +02:00
switch buildStatus {
2024-08-01 18:10:45 +02:00
case BuildStatusSummarySuccess:
2024-07-31 16:52:02 +02:00
_, err := h.AddReviewComment(pr, common.ReviewStateApproved, "Build successful")
if err != nil {
h.LogPlainError(err)
}
2024-08-01 18:10:45 +02:00
case BuildStatusSummaryFailed:
2024-07-31 16:52:02 +02:00
_, err := h.AddReviewComment(pr, common.ReviewStateRequestChanges, "Build failed")
if err != nil {
h.LogPlainError(err)
}
}
2024-08-16 14:04:43 +02:00
h.Log("Build status waiting: %d", buildStatus)
2024-08-01 18:10:45 +02:00
// waiting for build results -- nothing to do
2024-07-31 16:52:02 +02:00
2024-07-18 23:36:41 +02:00
case common.ReviewStateApproved:
2024-07-29 15:28:03 +02:00
// done, mark notification as read
h.Log("processing request for success build ...")
2024-08-05 17:00:39 +02:00
h.SetNotificationRead(thread.ID)
2024-07-31 16:52:02 +02:00
2024-07-18 23:36:41 +02:00
case common.ReviewStateRequestChanges:
2024-07-26 16:53:09 +02:00
// build failures, nothing to do here, mark notification as read
2024-07-29 15:28:03 +02:00
h.Log("processing request for failed request changes...")
2024-08-05 17:00:39 +02:00
h.SetNotificationRead(thread.ID)
2024-07-18 23:36:41 +02:00
}
2024-07-27 20:57:18 +02:00
break
2024-07-18 23:36:41 +02:00
}
}
func pollWorkNotifications() {
2024-07-26 16:53:09 +02:00
h := common.CreateRequestHandler(GitAuthor, BotName)
2024-08-05 17:00:39 +02:00
data, err := h.GetPullNotifications(nil)
2024-07-18 16:43:27 +02:00
if err != nil {
h.LogPlainError(err)
return
}
if data != nil {
for _, notification := range data {
2024-07-18 23:36:41 +02:00
switch notification.Subject.Type {
case "Pull":
2024-07-31 17:17:07 +02:00
processPullNotification(h, notification)
2024-07-18 23:36:41 +02:00
default:
h.SetNotificationRead(notification.ID)
}
2024-07-18 16:43:27 +02:00
}
}
}
2024-07-16 17:05:43 +02:00
func main() {
2024-07-18 23:36:41 +02:00
failOnError(common.RequireGiteaSecretToken(), "Cannot find GITEA_TOKEN")
failOnError(common.RequireObsSecretToken(), "Cannot find OBS_USER and OBS_PASSWORD")
2024-07-16 22:05:44 +02:00
2024-07-18 23:36:41 +02:00
// go ProcessingObsMessages("rabbit.opensuse.org", "opensuse", "opensuse", "")
2024-07-18 16:43:27 +02:00
2024-08-16 14:04:43 +02:00
for {
pollWorkNotifications()
time.Sleep(10 * time.Minute)
}
2024-07-17 17:20:24 +02:00
2024-07-18 23:36:41 +02:00
stuck := make(chan int)
2024-07-17 17:20:24 +02:00
<-stuck
2024-07-07 21:08:41 +02:00
}