2
1
forked from adamm/autogits

9 Commits

Author SHA256 Message Date
a576c98f68 obs_staging: fix results compare
Esp. with multiple repositories:
* j counter was not reset
* wrong limit of results used (from reference not stage project)
* package name compare using == was always wrong (and unneeded here)
2025-09-02 14:06:22 +02:00
c40d93d1f9 Staging: include also changed directories
Projects which are not using submodules as package directories
still need to be handled in stagings.
2025-09-01 17:33:16 +02:00
e2abbfcc63 staging: improve cleanup logging 2025-09-01 12:49:55 +02:00
f6cb35acca spec: add obs-staging-bot.service 2025-09-01 12:29:29 +02:00
f4386c3d12 Try to use Staging Master Project as default build target if available
This allows us to set custom build configuration or repository sets for
pull request projects.
2025-09-01 11:52:30 +02:00
f8594af8c6 obs-status: report error on monitor page if error
If we have error with REDIS connection, report it as error 500
on the / default page. Otherwise, report the 404 there instead
as before.
2025-09-01 11:20:54 +02:00
b8ef69a5a7 group-review: react on comment events
Instead of just polling for events, we can use issue_comment events
to process PRs more quickly.

At same time increased default polling interval to 10 minutes if
we use events

Closes #67
2025-08-30 10:41:29 +02:00
c980b9f84d group-review: improve comment made by the bot
Bot name should be expanded for easy copy-pasta
2025-08-29 18:19:03 +02:00
4651440457 Revert "Fixing creation or PR even when we don't want it"
This reverts commit e90ba95869.

We need to assign reviews anyway...
2025-08-29 17:09:08 +02:00
8 changed files with 121 additions and 46 deletions

View File

@@ -120,6 +120,7 @@ install -D -m0755 gitea-events-rabbitmq-publisher/gitea-events-rabbitmq-publishe
install -D -m0644 systemd/gitea-events-rabbitmq-publisher.service %{buildroot}%{_unitdir}/gitea-events-rabbitmq-publisher.service
install -D -m0755 group-review/group-review %{buildroot}%{_bindir}/group-review
install -D -m0755 obs-staging-bot/obs-staging-bot %{buildroot}%{_bindir}/obs-staging-bot
install -D -m0644 systemd/obs-staging-bot.service %{buildroot}%{_unitdir}/obs-staging-bot.service
install -D -m0755 obs-status-service/obs-status-service %{buildroot}%{_bindir}/obs-status-service
install -D -m0755 workflow-direct/workflow-direct %{buildroot}%{_bindir}/workflow-direct
install -D -m0755 workflow-pr/workflow-pr %{buildroot}%{_bindir}/workflow-pr
@@ -137,6 +138,18 @@ install -D -m0755 hujson/hujson
%postun -n gitea-events-rabbitmq-publisher
%service_del_postun gitea-events-rabbitmq-publisher.service
%pre -n obs-staging-bot
%service_add_pre obs-staging-bot.service
%post -n obs-staging-bot
%service_add_post obs-staging-bot.service
%preun -n obs-staging-bot
%service_del_preun obs-staging-bot.service
%postun -n obs-staging-bot
%service_del_postun obs-staging-bot.service
%files -n gitea-events-rabbitmq-publisher
%license COPYING
%doc gitea-events-rabbitmq-publisher/README.md
@@ -161,6 +174,7 @@ install -D -m0755 hujson/hujson
%license COPYING
%doc obs-staging-bot/README.md
%{_bindir}/obs-staging-bot
%{_unitdir}/obs-staging-bot.service
%files -n obs-status-service
%license COPYING

View File

@@ -821,6 +821,7 @@ func (e *GitHandlerImpl) GitSubmoduleList(gitPath, commitId string) (submoduleLi
for _, te := range tree.items {
if te.isTree() {
trees[p+te.name+"/"] = te.hash
submoduleList[p+te.name] = te.hash
} else if te.isSubmodule() {
submoduleList[p+te.name] = te.hash
}

View File

@@ -174,6 +174,22 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
return
}
if err := ProcessPR(pr); err == nil && !common.IsDryRun {
if err := gitea.SetNotificationRead(notification.ID); err != nil {
common.LogDebug(" Cannot set notification as read", err)
}
} else if err != nil && err != ReviewNotFinished {
common.LogError(err)
}
}
var ReviewNotFinished = fmt.Errorf("Review is not finished")
func ProcessPR(pr *models.PullRequest) error {
org := pr.Base.Repo.Owner.UserName
repo := pr.Base.Repo.Name
id := pr.Index
found := false
for _, reviewer := range pr.RequestedReviewers {
if reviewer != nil && reviewer.UserName == groupName {
@@ -183,42 +199,32 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
}
if !found {
common.LogInfo(" review is not requested for", groupName)
if !common.IsDryRun {
gitea.SetNotificationRead(notification.ID)
}
return
return nil
}
config := configs.GetPrjGitConfig(org, repo, pr.Base.Name)
if config == nil {
common.LogError("Cannot find config for:", fmt.Sprintf("%s/%s!%s", org, repo, pr.Base.Name))
return
return fmt.Errorf("Cannot find config for: %s", pr.URL)
}
if pr.State == "closed" {
// dismiss the review
common.LogInfo(" -- closed request, so nothing to review")
if !common.IsDryRun {
gitea.SetNotificationRead(notification.ID)
}
return
return nil
}
reviews, err := gitea.GetPullRequestReviews(org, repo, id)
if err != nil {
common.LogInfo(" ** No reviews associated with request:", subject.URL, "Error:", err)
return
return fmt.Errorf("Failed to fetch reviews for: %v: %w", pr.URL, err)
}
timeline, err := common.FetchTimelineSinceReviewRequestOrPush(gitea, groupName, pr.Head.Sha, org, repo, id)
if err != nil {
common.LogError(err)
return
return fmt.Errorf("Failed to fetch timeline to review. %w", err)
}
groupConfig, err := config.GetReviewGroup(groupName)
if err != nil {
common.LogError(err)
return
return fmt.Errorf("Failed to fetch review group. %w", err)
}
// submitter cannot be reviewer
@@ -238,13 +244,10 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
}
UnrequestReviews(gitea, org, repo, id, requestReviewers)
}
if err := gitea.SetNotificationRead(notification.ID); err != nil {
common.LogDebug(" Cannot set notification as read", err)
}
}
common.LogInfo(" -> approved by", reviewer)
common.LogInfo(" review at", review.Created)
return
return nil
} else if ReviewRejected(review.Body) {
if !common.IsDryRun {
text := reviewer + " requested changes on behalf of " + groupName + ". See " + review.HTMLURL
@@ -255,12 +258,9 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
}
UnrequestReviews(gitea, org, repo, id, requestReviewers)
}
if err := gitea.SetNotificationRead(notification.ID); err != nil {
common.LogDebug(" Cannot set notification as read", err)
}
}
common.LogInfo(" -> declined by", reviewer)
return
return nil
}
}
}
@@ -290,15 +290,22 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
}
if !found_help_comment && !common.IsDryRun {
helpComment := fmt.Sprintln("Review by", groupName, "represents a group of reviewers:", strings.Join(requestReviewers, ", "), ".\n\nDo **not** use standard review interface to review on behalf of the group.\nTo accept the review on behalf of the group, create the following comment: `@<bot>: approve`.\nTo request changes on behalf of the group, create the following comment: `@<bot>: decline` followed with lines justifying the decision.\nFuture edits of the comments are ignored, a new comment is required to change the review state.")
helpComment := fmt.Sprintln("Review by", groupName, "represents a group of reviewers:", strings.Join(requestReviewers, ", "), ".\n\n"+
"Do **not** use standard review interface to review on behalf of the group.\n"+
"To accept the review on behalf of the group, create the following comment: `@"+groupName+": approve`.\n"+
"To request changes on behalf of the group, create the following comment: `@"+groupName+": decline` followed with lines justifying the decision.\n"+
"Future edits of the comments are ignored, a new comment is required to change the review state.")
if slices.Contains(groupConfig.Reviewers, pr.User.UserName) {
helpComment = helpComment + "\n\n" + fmt.Sprintln("Submitter is member of this review group, hence they are excluded from being one of the reviewers here")
helpComment = helpComment + "\n\n" +
"Submitter is member of this review group, hence they are excluded from being one of the reviewers here"
}
gitea.AddComment(pr, helpComment)
}
return ReviewNotFinished
}
func PeriodReviewCheck(gitea common.Gitea) {
func PeriodReviewCheck() {
notifications, err := gitea.GetNotifications(common.GiteaNotificationType_Pull, nil)
if err != nil {
common.LogError(" Error fetching unread notifications: %w", err)
@@ -307,14 +314,15 @@ func PeriodReviewCheck(gitea common.Gitea) {
for _, notification := range notifications {
ProcessNotifications(notification, gitea)
}
}
var gitea common.Gitea
func main() {
giteaUrl := flag.String("gitea-url", "https://src.opensuse.org", "Gitea instance used for reviews")
rabbitMqHost := flag.String("rabbit-url", "amqps://rabbit.opensuse.org", "RabbitMQ instance where Gitea webhook notifications are sent")
interval := flag.Int64("interval", 5, "Notification polling interval in minutes (min 1 min)")
interval := flag.Int64("interval", 10, "Notification polling interval in minutes (min 1 min)")
configFile := flag.String("config", "", "PrjGit listing config file")
logging := flag.String("logging", "info", "Logging level: [none, error, info, debug]")
flag.BoolVar(&common.IsDryRun, "dry", false, "Dry run, no effect. For debugging")
@@ -351,7 +359,7 @@ func main() {
return
}
gitea := common.AllocateGiteaTransport(*giteaUrl)
gitea = common.AllocateGiteaTransport(*giteaUrl)
configs, err = common.ResolveWorkflowConfigs(gitea, configData)
if err != nil {
common.LogError("Cannot parse workflow configs:", err)
@@ -395,10 +403,13 @@ func main() {
config_modified: make(chan *common.AutogitConfig),
}
process_issue_pr := IssueCommentProcessor{}
configUpdates := &common.RabbitMQGiteaEventsProcessor{
Orgs: []string{},
Handlers: map[string]common.RequestProcessor{
common.RequestType_Push: &config_update,
common.RequestType_Push: &config_update,
common.RequestType_IssueComment: &process_issue_pr,
},
}
configUpdates.Connection().RabbitURL = u
@@ -435,7 +446,7 @@ func main() {
}
}
PeriodReviewCheck(gitea)
PeriodReviewCheck()
time.Sleep(time.Duration(*interval * int64(time.Minute)))
}
}

View File

@@ -7,6 +7,25 @@ import (
"src.opensuse.org/autogits/common"
)
type IssueCommentProcessor struct{}
func (s *IssueCommentProcessor) ProcessFunc(req *common.Request) error {
if req.Type != common.RequestType_IssueComment {
return fmt.Errorf("Unhandled, ignored request type: %s", req.Type)
}
data := req.Data.(*common.IssueCommentWebhookEvent)
org := data.Repository.Owner.Username
repo := data.Repository.Name
index := int64(data.Issue.Number)
pr, err := gitea.GetPullRequest(org, repo, index)
if err != nil {
return fmt.Errorf("Failed to fetch PullRequest from event: %s/%s!%d Error: %w", org, repo, index, err)
}
return ProcessPR(pr)
}
type ConfigUpdatePush struct {
config_modified chan *common.AutogitConfig
}

View File

@@ -222,7 +222,6 @@ func ProcessRepoBuildStatus(results, ref []*common.PackageBuildStatus) (status B
slices.SortFunc(results, PackageBuildStatusSorter)
slices.SortFunc(ref, PackageBuildStatusSorter)
j := 0
SomeSuccess = false
for i := 0; i < len(results); i++ {
res, ok := common.ObsBuildStatusDetails[results[i].Code]
@@ -237,10 +236,11 @@ func ProcessRepoBuildStatus(results, ref []*common.PackageBuildStatus) (status B
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++ {
j := 0
for ; j < len(ref) && strings.Compare(results[i].Package, ref[j].Package) != 0; j++ {
}
if j < len(results) && results[i].Package == ref[j].Package {
if j < len(ref) {
refRes, ok := common.ObsBuildStatusDetails[ref[j].Code]
if !ok {
common.LogInfo("unknown ref package result code:", ref[j].Code, "package:", ref[j].Package)
@@ -263,7 +263,7 @@ func ProcessRepoBuildStatus(results, ref []*common.PackageBuildStatus) (status B
return BuildStatusSummarySuccess, SomeSuccess
}
func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingPrj, buildPrj string) (*common.ProjectMeta, error) {
func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingPrj, buildPrj string, stagingMasterPrj string) (*common.ProjectMeta, error) {
common.LogDebug("repo content fetching ...")
err := FetchPrGit(git, pr)
if err != nil {
@@ -289,7 +289,15 @@ func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullReque
}
}
meta, err := ObsClient.GetProjectMeta(buildPrj)
common.LogDebug("Trying first staging master project: ", stagingMasterPrj)
meta, err := ObsClient.GetProjectMeta(stagingMasterPrj)
if err == nil {
// success, so we use that staging master project as our build project
buildPrj = stagingMasterPrj
} else {
common.LogInfo("error fetching project meta for ", stagingMasterPrj, ". Fall Back to ", buildPrj)
meta, err = ObsClient.GetProjectMeta(buildPrj)
}
if err != nil {
common.LogError("error fetching project meta for", buildPrj, ". Err:", err)
return nil, err
@@ -414,7 +422,8 @@ func StartOrUpdateBuild(config *common.StagingConfig, git common.Git, gitea comm
var state RequestModification = RequestModificationSourceChanged
if meta == nil {
// new build
meta, err = GenerateObsPrjMeta(git, gitea, pr, obsPrProject, config.ObsProject)
common.LogDebug(" Staging master:", config.StagingProject)
meta, err = GenerateObsPrjMeta(git, gitea, pr, obsPrProject, config.ObsProject, config.StagingProject)
if err != nil {
return RequestModificationNoChange, err
}
@@ -428,6 +437,8 @@ func StartOrUpdateBuild(config *common.StagingConfig, git common.Git, gitea comm
} else {
err = ObsClient.SetProjectMeta(meta)
if err != nil {
x, _ := xml.MarshalIndent(meta, "", " ")
common.LogDebug(" meta:", string(x))
common.LogError("cannot create meta project:", err)
return RequestModificationNoChange, err
}
@@ -584,7 +595,7 @@ func CleanupPullNotification(gitea common.Gitea, thread *models.NotificationThre
}
if !pr.HasMerged && time.Since(time.Time(pr.Closed)) < time.Duration(config.CleanupDelay)*time.Hour {
common.LogInfo("Cooldown period for cleanup of", thread.URL)
common.LogInfo("Cooldown period for cleanup of", thread.Subject.HTMLURL)
return false
}

View File

@@ -191,15 +191,24 @@ func main() {
return
}
var rescanRepoError error
go func() {
for {
if err := RescanRepositories(); err != nil {
if rescanRepoError = RescanRepositories(); rescanRepoError != nil {
common.LogError("Failed to rescan repositories.", err)
}
time.Sleep(time.Minute * 5)
}
}()
http.HandleFunc("GET /", func(res http.ResponseWriter, req *http.Request) {
if rescanRepoError != nil {
res.WriteHeader(500)
return
}
res.WriteHeader(404)
res.Write([]byte("404 page not found\n"))
})
http.HandleFunc("GET /status/{Project}", func(res http.ResponseWriter, req *http.Request) {
obsPrj := req.PathValue("Project")
common.LogInfo(" request: GET /status/" + obsPrj)

View File

@@ -0,0 +1,16 @@
[Unit]
Description=Staging bot for project git PRs in OBS
After=network-online.target
[Service]
Type=exec
ExecStart=/usr/bin/obs-staging-bot
EnvironmentFile=-/etc/sysconfig/obs-staging-bot.env
DynamicUser=yes
NoNewPrivileges=yes
ProtectSystem=strict
[Install]
WantedBy=multi-user.target

View File

@@ -367,13 +367,7 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
prjGitPRbranch := prGitBranchNameForPR(prRepo, prNo)
prjGitPR, err := prset.GetPrjGitPR()
if err == common.PRSet_PrjGitMissing {
if config.NoProjectGitPR {
common.LogDebug("No Project Git PR yet and we are set to not create it.")
return nil
}
common.LogDebug("Missing PrjGit. Need to create one under branch", prjGitPRbranch)
if err = pr.CreatePRjGitPR(prjGitPRbranch, prset); err != nil {