368 lines
10 KiB
Go
368 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"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")
|
|
|
|
/*
|
|
'published' => 'Repository has been published',
|
|
'publishing' => 'Repository is being created right now',
|
|
'unpublished' => 'Build finished, but repository publishing is disabled',
|
|
'building' => 'Build jobs exist for the repository',
|
|
'finished' => 'Build jobs have been processed, new repository is not yet created',
|
|
'blocked' => 'No build possible at the moment, waiting for jobs in other repositories',
|
|
'broken' => 'The repository setup is broken, build or publish not possible',
|
|
'scheduling' => 'The repository state is being calculated right now'
|
|
*/
|
|
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 ...")
|
|
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)
|
|
return h.Error
|
|
}
|
|
return fmt.Errorf("Cannot find reference project for %s PR#%d", pr.Base.Name, pr.Index)
|
|
}
|
|
if h.HasError() {
|
|
h.LogPlainError(h.Error)
|
|
return h.Error
|
|
}
|
|
|
|
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
|
|
|
|
meta.Name = getObsProjectAssociatedWithPr(obsClient.HomeProject, pr)
|
|
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,
|
|
)
|
|
|
|
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, "&") + "#" + pr.Head.Sha
|
|
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)
|
|
|
|
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/(?<org>[a-zA-Z0-9]+)/(?<project>[_a-zA-Z0-9]+)/issues/(?<num>[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 {
|
|
if errors.Is(err, common.ObsProjectNotFound{Project: obsProject}) {
|
|
// recreate missing project
|
|
h.LogError("missing OBS project ... recreating '%s': %v", obsProject, err)
|
|
startBuild(h, pr, obsClient)
|
|
return
|
|
}
|
|
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
|
|
}
|