14 Commits
main ... main

Author SHA256 Message Date
168a419bbe status: allow for package search endpoint
OBS has issues searching for packages in scmsynced projects.
Since we have a list of all the repositories, we can allow
for a search endpoint here.

/search?q=term1&q=term2...

results is JSON

[
   project1/pkgA,
   project2/pkgB
]
2025-09-03 14:35:15 +02:00
6a71641295 common: take care of empty result sets
In case of empty result pages, we should ignore the X-Total-Count
header.

Fixes: 5addde0a71
2025-09-03 12:21:07 +02:00
5addde0a71 common: use X-Total-Count in multi-page results 2025-09-03 01:00:33 +02:00
90ea1c9463 common: remove duplicate 2025-09-02 20:50:23 +02:00
a4fb3e6151 PR: Don't clobber other's PrjGit description
If we did not create the PRjGit PR, don't touch the title
and description

Closes: #68
2025-09-02 19:47:47 +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
7d58882ed8 Accept review (instead of removal) on no submodule change
Because we require approval by staging bot in the workflow bot.
2025-08-29 15:08:05 +02:00
e90ba95869 Fixing creation or PR even when we don't want it 2025-08-29 15:08:05 +02:00
9 changed files with 249 additions and 68 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 -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 group-review/group-review %{buildroot}%{_bindir}/group-review
install -D -m0755 obs-staging-bot/obs-staging-bot %{buildroot}%{_bindir}/obs-staging-bot 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 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-direct/workflow-direct %{buildroot}%{_bindir}/workflow-direct
install -D -m0755 workflow-pr/workflow-pr %{buildroot}%{_bindir}/workflow-pr 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 %postun -n gitea-events-rabbitmq-publisher
%service_del_postun gitea-events-rabbitmq-publisher.service %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 %files -n gitea-events-rabbitmq-publisher
%license COPYING %license COPYING
%doc gitea-events-rabbitmq-publisher/README.md %doc gitea-events-rabbitmq-publisher/README.md
@@ -161,6 +174,7 @@ install -D -m0755 hujson/hujson
%license COPYING %license COPYING
%doc obs-staging-bot/README.md %doc obs-staging-bot/README.md
%{_bindir}/obs-staging-bot %{_bindir}/obs-staging-bot
%{_unitdir}/obs-staging-bot.service
%files -n obs-status-service %files -n obs-status-service
%license COPYING %license COPYING

View File

@@ -24,11 +24,13 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"net/http"
"net/url" "net/url"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"slices" "slices"
"strconv"
"time" "time"
transport "github.com/go-openapi/runtime/client" transport "github.com/go-openapi/runtime/client"
@@ -182,7 +184,6 @@ type Gitea interface {
GiteaCommitStatusGetter GiteaCommitStatusGetter
GiteaCommitStatusSetter GiteaCommitStatusSetter
GiteaSetRepoOptions GiteaSetRepoOptions
GiteaTimelineFetcher
GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error) GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error)
GetDoneNotifications(Type string, page int64) ([]*models.NotificationThread, error) GetDoneNotifications(Type string, page int64) ([]*models.NotificationThread, error)
@@ -199,7 +200,32 @@ type Gitea interface {
GetCurrentUser() (*models.User, error) GetCurrentUser() (*models.User, error)
} }
type GiteaHeaderInterceptor struct {
Length int
http.RoundTripper
}
func (i *GiteaHeaderInterceptor) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := i.RoundTripper.RoundTrip(req)
if err != nil {
return nil, err
}
count_header := resp.Header["X-Total-Count"]
if len(count_header) == 1 {
i.Length, err = strconv.Atoi(resp.Header["X-Total-Count"][0])
if err != nil {
LogError("Converting X-Total-Count response header error", err)
i.Length = -1
return nil, err
}
} else {
i.Length = -1
}
return resp, nil
}
type GiteaTransport struct { type GiteaTransport struct {
headers *GiteaHeaderInterceptor
transport *transport.Runtime transport *transport.Runtime
client *apiclient.GiteaAPI client *apiclient.GiteaAPI
} }
@@ -212,7 +238,9 @@ func AllocateGiteaTransport(giteaUrl string) Gitea {
log.Panicln("Failed to parse gitea url:", err) log.Panicln("Failed to parse gitea url:", err)
} }
r.headers = &GiteaHeaderInterceptor{RoundTripper: http.DefaultTransport}
r.transport = transport.New(url.Host, apiclient.DefaultBasePath, [](string){url.Scheme}) r.transport = transport.New(url.Host, apiclient.DefaultBasePath, [](string){url.Scheme})
r.transport.Transport = r.headers
r.transport.DefaultAuthentication = transport.BearerToken(giteaToken) r.transport.DefaultAuthentication = transport.BearerToken(giteaToken)
r.client = apiclient.New(r.transport, nil) r.client = apiclient.New(r.transport, nil)
@@ -287,10 +315,9 @@ func (gitea *GiteaTransport) ManualMergePR(org, repo string, num int64, commitid
} }
func (gitea *GiteaTransport) GetPullRequests(org, repo string) ([]*models.PullRequest, error) { func (gitea *GiteaTransport) GetPullRequests(org, repo string) ([]*models.PullRequest, error) {
var page, limit int64 var page int64
prs := make([]*models.PullRequest, 0) prs := make([]*models.PullRequest, 0)
limit = 20
state := "open" state := "open"
for { for {
@@ -302,16 +329,18 @@ func (gitea *GiteaTransport) GetPullRequests(org, repo string) ([]*models.PullRe
WithOwner(org). WithOwner(org).
WithRepo(repo). WithRepo(repo).
WithState(&state). WithState(&state).
WithPage(&page). WithPage(&page),
WithLimit(&limit),
gitea.transport.DefaultAuthentication) gitea.transport.DefaultAuthentication)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot fetch PR list for %s / %s : %w", org, repo, err) return nil, fmt.Errorf("cannot fetch PR list for %s / %s : %w", org, repo, err)
} }
if len(req.Payload) == 0 {
break
}
prs = slices.Concat(prs, req.Payload) prs = slices.Concat(prs, req.Payload)
if len(req.Payload) < int(limit) { if len(prs) >= gitea.headers.Length {
break break
} }
} }
@@ -320,21 +349,23 @@ func (gitea *GiteaTransport) GetPullRequests(org, repo string) ([]*models.PullRe
} }
func (gitea *GiteaTransport) GetCommitStatus(org, repo, hash string) ([]*models.CommitStatus, error) { func (gitea *GiteaTransport) GetCommitStatus(org, repo, hash string) ([]*models.CommitStatus, error) {
page := int64(1) var page int64
limit := int64(10)
var res []*models.CommitStatus var res []*models.CommitStatus
for { for {
page++
r, err := gitea.client.Repository.RepoListStatuses( r, err := gitea.client.Repository.RepoListStatuses(
repository.NewRepoListStatusesParams().WithDefaults().WithOwner(org).WithRepo(repo).WithSha(hash).WithPage(&page).WithLimit(&limit), repository.NewRepoListStatusesParams().WithDefaults().WithOwner(org).WithRepo(repo).WithSha(hash).WithPage(&page),
gitea.transport.DefaultAuthentication) gitea.transport.DefaultAuthentication)
if err != nil { if err != nil {
return res, err return res, err
} }
if len(r.Payload) == 0 {
break
}
res = append(res, r.Payload...) res = append(res, r.Payload...)
if len(r.Payload) < int(limit) { if len(res) >= gitea.headers.Length {
break break
} }
} }
@@ -377,19 +408,18 @@ func (gitea *GiteaTransport) GetRepository(org, pkg string) (*models.Repository,
} }
func (gitea *GiteaTransport) GetPullRequestReviews(org, project string, PRnum int64) ([]*models.PullReview, error) { func (gitea *GiteaTransport) GetPullRequestReviews(org, project string, PRnum int64) ([]*models.PullReview, error) {
limit := int64(20)
var page int64 var page int64
var allReviews []*models.PullReview var allReviews []*models.PullReview
for { for {
page++
reviews, err := gitea.client.Repository.RepoListPullReviews( reviews, err := gitea.client.Repository.RepoListPullReviews(
repository.NewRepoListPullReviewsParams(). repository.NewRepoListPullReviewsParams().
WithDefaults(). WithDefaults().
WithOwner(org). WithOwner(org).
WithRepo(project). WithRepo(project).
WithIndex(PRnum). WithIndex(PRnum).
WithPage(&page). WithPage(&page),
WithLimit(&limit),
gitea.transport.DefaultAuthentication, gitea.transport.DefaultAuthentication,
) )
@@ -397,11 +427,13 @@ func (gitea *GiteaTransport) GetPullRequestReviews(org, project string, PRnum in
return nil, err return nil, err
} }
allReviews = slices.Concat(allReviews, reviews.Payload) if len(reviews.Payload) == 0 {
if len(reviews.Payload) < int(limit) { break
}
allReviews = slices.Concat(allReviews, reviews.Payload)
if len(allReviews) >= gitea.headers.Length {
break break
} }
page++
} }
return allReviews, nil return allReviews, nil
@@ -469,7 +501,6 @@ const (
) )
func (gitea *GiteaTransport) GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error) { func (gitea *GiteaTransport) GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error) {
bigLimit := int64(20)
ret := make([]*models.NotificationThread, 0, 100) ret := make([]*models.NotificationThread, 0, 100)
for page := int64(1); ; page++ { for page := int64(1); ; page++ {
@@ -477,7 +508,6 @@ func (gitea *GiteaTransport) GetNotifications(Type string, since *time.Time) ([]
WithDefaults(). WithDefaults().
WithSubjectType([]string{Type}). WithSubjectType([]string{Type}).
WithStatusTypes([]string{"unread"}). WithStatusTypes([]string{"unread"}).
WithLimit(&bigLimit).
WithPage(&page) WithPage(&page)
if since != nil { if since != nil {
@@ -490,8 +520,11 @@ func (gitea *GiteaTransport) GetNotifications(Type string, since *time.Time) ([]
return nil, err return nil, err
} }
if len(list.Payload) == 0 {
break
}
ret = slices.Concat(ret, list.Payload) ret = slices.Concat(ret, list.Payload)
if len(list.Payload) < int(bigLimit) { if len(ret) >= gitea.headers.Length {
break break
} }
} }
@@ -500,7 +533,6 @@ func (gitea *GiteaTransport) GetNotifications(Type string, since *time.Time) ([]
} }
func (gitea *GiteaTransport) GetDoneNotifications(Type string, page int64) ([]*models.NotificationThread, error) { func (gitea *GiteaTransport) GetDoneNotifications(Type string, page int64) ([]*models.NotificationThread, error) {
limit := int64(20)
t := true t := true
if page <= 0 { if page <= 0 {
@@ -511,7 +543,6 @@ func (gitea *GiteaTransport) GetDoneNotifications(Type string, page int64) ([]*m
WithAll(&t). WithAll(&t).
WithSubjectType([]string{Type}). WithSubjectType([]string{Type}).
WithStatusTypes([]string{"read"}). WithStatusTypes([]string{"read"}).
WithLimit(&limit).
WithPage(&page), WithPage(&page),
gitea.transport.DefaultAuthentication) gitea.transport.DefaultAuthentication)
if err != nil { if err != nil {
@@ -564,9 +595,12 @@ func (gitea *GiteaTransport) GetOrganizationRepositories(orgName string) ([]*mod
if len(ret.Payload) == 0 { if len(ret.Payload) == 0 {
break break
} }
repos = append(repos, ret.Payload...) repos = append(repos, ret.Payload...)
page++ page++
if len(repos) >= gitea.headers.Length {
break
}
} }
return repos, nil return repos, nil
@@ -780,15 +814,18 @@ func (gitea *GiteaTransport) GetTimeline(org, repo string, idx int64) ([]*models
resCount = len(res.Payload) resCount = len(res.Payload)
LogDebug("page:", page, "len:", resCount) LogDebug("page:", page, "len:", resCount)
if resCount == 0 {
break
}
page++ page++
for _, d := range res.Payload { retData = append(retData, res.Payload...)
if d != nil { if len(retData) >= gitea.headers.Length {
retData = append(retData, d) break
}
} }
} }
LogDebug("total results:", len(retData)) LogDebug("total results:", len(retData))
retData = slices.DeleteFunc(retData, func(a *models.TimelineComment) bool { return a == nil })
slices.SortFunc(retData, func(a, b *models.TimelineComment) int { slices.SortFunc(retData, func(a, b *models.TimelineComment) int {
return time.Time(b.Created).Compare(time.Time(a.Created)) return time.Time(b.Created).Compare(time.Time(a.Created))
}) })

View File

@@ -174,6 +174,22 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
return 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 found := false
for _, reviewer := range pr.RequestedReviewers { for _, reviewer := range pr.RequestedReviewers {
if reviewer != nil && reviewer.UserName == groupName { if reviewer != nil && reviewer.UserName == groupName {
@@ -183,42 +199,32 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
} }
if !found { if !found {
common.LogInfo(" review is not requested for", groupName) common.LogInfo(" review is not requested for", groupName)
if !common.IsDryRun { return nil
gitea.SetNotificationRead(notification.ID)
}
return
} }
config := configs.GetPrjGitConfig(org, repo, pr.Base.Name) config := configs.GetPrjGitConfig(org, repo, pr.Base.Name)
if config == nil { if config == nil {
common.LogError("Cannot find config for:", fmt.Sprintf("%s/%s!%s", org, repo, pr.Base.Name)) return fmt.Errorf("Cannot find config for: %s", pr.URL)
return
} }
if pr.State == "closed" { if pr.State == "closed" {
// dismiss the review // dismiss the review
common.LogInfo(" -- closed request, so nothing to review") common.LogInfo(" -- closed request, so nothing to review")
if !common.IsDryRun { return nil
gitea.SetNotificationRead(notification.ID)
}
return
} }
reviews, err := gitea.GetPullRequestReviews(org, repo, id) reviews, err := gitea.GetPullRequestReviews(org, repo, id)
if err != nil { if err != nil {
common.LogInfo(" ** No reviews associated with request:", subject.URL, "Error:", err) return fmt.Errorf("Failed to fetch reviews for: %v: %w", pr.URL, err)
return
} }
timeline, err := common.FetchTimelineSinceReviewRequestOrPush(gitea, groupName, pr.Head.Sha, org, repo, id) timeline, err := common.FetchTimelineSinceReviewRequestOrPush(gitea, groupName, pr.Head.Sha, org, repo, id)
if err != nil { if err != nil {
common.LogError(err) return fmt.Errorf("Failed to fetch timeline to review. %w", err)
return
} }
groupConfig, err := config.GetReviewGroup(groupName) groupConfig, err := config.GetReviewGroup(groupName)
if err != nil { if err != nil {
common.LogError(err) return fmt.Errorf("Failed to fetch review group. %w", err)
return
} }
// submitter cannot be reviewer // submitter cannot be reviewer
@@ -238,13 +244,10 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
} }
UnrequestReviews(gitea, org, repo, id, requestReviewers) 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(" -> approved by", reviewer)
common.LogInfo(" review at", review.Created) common.LogInfo(" review at", review.Created)
return return nil
} else if ReviewRejected(review.Body) { } else if ReviewRejected(review.Body) {
if !common.IsDryRun { if !common.IsDryRun {
text := reviewer + " requested changes on behalf of " + groupName + ". See " + review.HTMLURL 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) 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) 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 { 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) { 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) gitea.AddComment(pr, helpComment)
} }
return ReviewNotFinished
} }
func PeriodReviewCheck(gitea common.Gitea) { func PeriodReviewCheck() {
notifications, err := gitea.GetNotifications(common.GiteaNotificationType_Pull, nil) notifications, err := gitea.GetNotifications(common.GiteaNotificationType_Pull, nil)
if err != nil { if err != nil {
common.LogError(" Error fetching unread notifications: %w", err) common.LogError(" Error fetching unread notifications: %w", err)
@@ -307,14 +314,15 @@ func PeriodReviewCheck(gitea common.Gitea) {
for _, notification := range notifications { for _, notification := range notifications {
ProcessNotifications(notification, gitea) ProcessNotifications(notification, gitea)
} }
} }
var gitea common.Gitea
func main() { func main() {
giteaUrl := flag.String("gitea-url", "https://src.opensuse.org", "Gitea instance used for reviews") 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") 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") configFile := flag.String("config", "", "PrjGit listing config file")
logging := flag.String("logging", "info", "Logging level: [none, error, info, debug]") logging := flag.String("logging", "info", "Logging level: [none, error, info, debug]")
flag.BoolVar(&common.IsDryRun, "dry", false, "Dry run, no effect. For debugging") flag.BoolVar(&common.IsDryRun, "dry", false, "Dry run, no effect. For debugging")
@@ -351,7 +359,7 @@ func main() {
return return
} }
gitea := common.AllocateGiteaTransport(*giteaUrl) gitea = common.AllocateGiteaTransport(*giteaUrl)
configs, err = common.ResolveWorkflowConfigs(gitea, configData) configs, err = common.ResolveWorkflowConfigs(gitea, configData)
if err != nil { if err != nil {
common.LogError("Cannot parse workflow configs:", err) common.LogError("Cannot parse workflow configs:", err)
@@ -395,10 +403,13 @@ func main() {
config_modified: make(chan *common.AutogitConfig), config_modified: make(chan *common.AutogitConfig),
} }
process_issue_pr := IssueCommentProcessor{}
configUpdates := &common.RabbitMQGiteaEventsProcessor{ configUpdates := &common.RabbitMQGiteaEventsProcessor{
Orgs: []string{}, Orgs: []string{},
Handlers: map[string]common.RequestProcessor{ 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 configUpdates.Connection().RabbitURL = u
@@ -435,7 +446,7 @@ func main() {
} }
} }
PeriodReviewCheck(gitea) PeriodReviewCheck()
time.Sleep(time.Duration(*interval * int64(time.Minute))) time.Sleep(time.Duration(*interval * int64(time.Minute)))
} }
} }

View File

@@ -7,6 +7,25 @@ import (
"src.opensuse.org/autogits/common" "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 { type ConfigUpdatePush struct {
config_modified chan *common.AutogitConfig config_modified chan *common.AutogitConfig
} }

View File

@@ -263,7 +263,7 @@ func ProcessRepoBuildStatus(results, ref []*common.PackageBuildStatus) (status B
return BuildStatusSummarySuccess, SomeSuccess 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 ...") common.LogDebug("repo content fetching ...")
err := FetchPrGit(git, pr) err := FetchPrGit(git, pr)
if err != nil { 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 { if err != nil {
common.LogError("error fetching project meta for", buildPrj, ". Err:", err) common.LogError("error fetching project meta for", buildPrj, ". Err:", err)
return nil, err return nil, err
@@ -414,7 +422,8 @@ func StartOrUpdateBuild(config *common.StagingConfig, git common.Git, gitea comm
var state RequestModification = RequestModificationSourceChanged var state RequestModification = RequestModificationSourceChanged
if meta == nil { if meta == nil {
// new build // 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 { if err != nil {
return RequestModificationNoChange, err return RequestModificationNoChange, err
} }
@@ -428,6 +437,8 @@ func StartOrUpdateBuild(config *common.StagingConfig, git common.Git, gitea comm
} else { } else {
err = ObsClient.SetProjectMeta(meta) err = ObsClient.SetProjectMeta(meta)
if err != nil { if err != nil {
x, _ := xml.MarshalIndent(meta, "", " ")
common.LogDebug(" meta:", string(x))
common.LogError("cannot create meta project:", err) common.LogError("cannot create meta project:", err)
return RequestModificationNoChange, 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 { 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 return false
} }
@@ -805,7 +816,12 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
if !rebuild_all { if !rebuild_all {
common.LogInfo("No package changes detected. Ignoring") common.LogInfo("No package changes detected. Ignoring")
if !IsDryRun { if !IsDryRun {
_, err = gitea.AddReviewComment(pr, common.ReviewStateComment, "No package changes. Not rebuilding project by default") _, err := gitea.AddReviewComment(pr, common.ReviewStateApproved, "No package changes, not rebuilding project by default, accepting change")
if err != nil {
common.LogError(err)
} else {
return true, nil
}
} }
return true, err return true, err
} }

View File

@@ -20,6 +20,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/json"
"flag" "flag"
"fmt" "fmt"
"io" "io"
@@ -191,15 +192,24 @@ func main() {
return return
} }
var rescanRepoError error
go func() { go func() {
for { for {
if err := RescanRepositories(); err != nil { if rescanRepoError = RescanRepositories(); rescanRepoError != nil {
common.LogError("Failed to rescan repositories.", err) common.LogError("Failed to rescan repositories.", err)
} }
time.Sleep(time.Minute * 5) 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) { http.HandleFunc("GET /status/{Project}", func(res http.ResponseWriter, req *http.Request) {
obsPrj := req.PathValue("Project") obsPrj := req.PathValue("Project")
common.LogInfo(" request: GET /status/" + obsPrj) common.LogInfo(" request: GET /status/" + obsPrj)
@@ -259,6 +269,32 @@ func main() {
res.Write(BuildStatusSvg(nil, &common.PackageBuildStatus{Package: pkg, Code: "unknown"})) res.Write(BuildStatusSvg(nil, &common.PackageBuildStatus{Package: pkg, Code: "unknown"}))
}) })
http.HandleFunc("GET /search", func(res http.ResponseWriter, req *http.Request) {
common.LogInfo("GET /serach?" + req.URL.RawQuery)
queries := req.URL.Query()
if !queries.Has("q") {
res.WriteHeader(400)
return
}
names := queries["q"]
if len(names) < 1 || len(names) > 10 {
res.WriteHeader(400)
return
}
packages := FindPackages(names)
data, err := json.MarshalIndent(packages, "", " ")
if err != nil {
res.WriteHeader(500)
common.LogError("Error in marshalling data.", err)
return
}
res.Write(data)
res.WriteHeader(200)
})
http.HandleFunc("GET /buildlog/{Project}/{Package}/{Repository}/{Arch}", func(res http.ResponseWriter, req *http.Request) { http.HandleFunc("GET /buildlog/{Project}/{Package}/{Repository}/{Arch}", func(res http.ResponseWriter, req *http.Request) {
prj := req.PathValue("Project") prj := req.PathValue("Project")
pkg := req.PathValue("Package") pkg := req.PathValue("Package")

View File

@@ -110,6 +110,33 @@ func FindRepoResults(project, repo string) []*common.BuildResult {
return ret return ret
} }
func FindPackages(search_terms []string) []string {
RepoStatusLock.RLock()
defer RepoStatusLock.RUnlock()
data := make([]string, 0, 100)
for _, repo := range RepoStatus {
for _, status := range repo.Status {
pkg := status.Package
match := true
for _, term := range search_terms {
match = match && strings.Contains(pkg, term)
}
if match {
entry := repo.Project + "/" + repo.Status[0].Package
if idx, found := slices.BinarySearch(data, entry); !found {
data = slices.Insert(data, idx, entry)
if len(data) >= 100 {
return data
}
}
}
}
}
return data
}
func FindAndUpdateProjectResults(project string) []*common.BuildResult { func FindAndUpdateProjectResults(project string) []*common.BuildResult {
res := FindProjectResults(project) res := FindProjectResults(project)
wg := &sync.WaitGroup{} wg := &sync.WaitGroup{}

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

@@ -317,9 +317,14 @@ func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
} }
PrjGitTitle, PrjGitBody := PrjGitDescription(prset) PrjGitTitle, PrjGitBody := PrjGitDescription(prset)
if PrjGitPR.PR.Title != PrjGitTitle || PrjGitPR.PR.Body != PrjGitBody { if PrjGitPR.PR.User.UserName == CurrentUser.UserName {
common.LogDebug("New title:", PrjGitTitle) if PrjGitPR.PR.Title != PrjGitTitle || PrjGitPR.PR.Body != PrjGitBody {
common.LogDebug(PrjGitBody) common.LogDebug("New title:", PrjGitTitle)
common.LogDebug(PrjGitBody)
}
} else {
// TODO: find our first comment in timeline
} }
if !common.IsDryRun { if !common.IsDryRun {