to use ! instead of # . The later is for issues and only works due to a redirect, which currently fails in gitea if a repo has its issue tracker disabled.
294 lines
8.1 KiB
Go
294 lines
8.1 KiB
Go
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/(?<org>[_a-zA-Z0-9-]+)/(?<project>[_a-zA-Z0-9-]+)/(?:issues|pulls)/(?<num>[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)
|
|
}
|
|
}
|