2 Commits

Author SHA256 Message Date
Jan Zerebecki
ff217cef01 Add go generate result 2025-08-26 16:15:17 +02:00
Gitea Actions
61c59ed3e1 CI run result of: go mod vendor 2025-08-26 16:15:14 +02:00
31 changed files with 186 additions and 705 deletions

View File

@@ -1,30 +0,0 @@
name: go-generate-check
on:
push:
branches: ['main']
pull_request:
paths:
- '**.go'
- '**.mod'
- '**.sum'
workflow_dispatch:
jobs:
go-generate-check:
name: go-generate-check
container:
image: registry.opensuse.org/home/jzerebecki/branches/devel/factory/git-workflow/containers/opensuse/bci/golang-extended:latest
steps:
- run: git clone --no-checkout --depth 1 ${{ gitea.server_url }}/${{ gitea.repository }} .
- run: git fetch origin ${{ gitea.ref }}
- run: git checkout FETCH_HEAD
- run: go generate -C common
- run: go generate -C workflow-pr
- run: go generate -C workflow-pr/interfaces
- run: git add -N .; git diff
- run: |
status=$(git status --short)
if [[ -n "$status" ]]; then
echo -e "$status"
echo "Please commit the differences from running: go generate"
false
fi

View File

@@ -1,25 +0,0 @@
name: go-generate-push
on:
workflow_dispatch:
jobs:
go-generate-push:
name: go-generate-push
container:
image: registry.opensuse.org/home/jzerebecki/branches/devel/factory/git-workflow/containers/opensuse/bci/golang-extended:latest
steps:
- run: git clone --no-checkout --depth 1 ${{ gitea.server_url }}/${{ gitea.repository }} .
- run: git fetch origin ${{ gitea.ref }}
- run: git checkout FETCH_HEAD
- run: go generate -C common
- run: go generate -C workflow-pr
- run: go generate -C workflow-pr/interfaces
- run: |
host=${{ gitea.server_url }}
host=${host#https://}
echo $host
git remote set-url origin "https://x-access-token:${{ secrets.GITEA_TOKEN }}@$host/${{ gitea.repository }}"
git config user.name "Gitea Actions"
git config user.email "gitea_noreply@opensuse.org"
- run: 'git status --short; git status --porcelain=2|grep --quiet -v . || ( git add .;git commit -m "CI run result of: go generate"; git push origin HEAD:${{ gitea.ref }} )'
- run: git log -p FETCH_HEAD...HEAD
- run: git log --numstat FETCH_HEAD...HEAD

View File

@@ -1,30 +0,0 @@
name: go-vendor-check
on:
push:
branches: ['main']
pull_request:
paths:
- '**.mod'
- '**.sum'
workflow_dispatch:
jobs:
go-generate-check:
name: go-vendor-check
container:
image: registry.opensuse.org/home/jzerebecki/branches/devel/factory/git-workflow/containers/opensuse/bci/golang-extended:latest
steps:
- run: git clone --no-checkout --depth 1 ${{ gitea.server_url }}/${{ gitea.repository }} .
- run: git fetch origin ${{ gitea.ref }}
- run: git checkout FETCH_HEAD
- run: go mod download
- run: go mod vendor
- run: go mod verify
- run: git add -N .; git diff
- run: go mod tidy -diff || true
- run: |
status=$(git status --short)
if [[ -n "$status" ]]; then
echo -e "$status"
echo "Please commit the differences from running: go generate"
false
fi

View File

@@ -1,26 +0,0 @@
name: go-generate-push
on:
workflow_dispatch:
jobs:
go-generate-push:
name: go-generate-push
container:
image: registry.opensuse.org/home/jzerebecki/branches/devel/factory/git-workflow/containers/opensuse/bci/golang-extended:latest
steps:
- run: git clone --no-checkout --depth 1 ${{ gitea.server_url }}/${{ gitea.repository }} .
- run: git fetch origin ${{ gitea.ref }}
- run: git checkout FETCH_HEAD
- run: go mod download
- run: go mod vendor
- run: go mod verify
- run: |
host=${{ gitea.server_url }}
host=${host#https://}
echo $host
git remote set-url origin "https://x-access-token:${{ secrets.GITEA_TOKEN }}@$host/${{ gitea.repository }}"
git config user.name "Gitea Actions"
git config user.email "gitea_noreply@opensuse.org"
- run: 'git status --short; git status --porcelain=2|grep --quiet -v . || ( git add .;git commit -m "CI run result of: go mod vendor"; git push origin HEAD:${{ gitea.ref }} )'
- run: go mod tidy -diff || true
- run: git log -p FETCH_HEAD...HEAD
- run: git log --numstat FETCH_HEAD...HEAD

3
.gitignore vendored
View File

@@ -1,2 +1,5 @@
node_modules
*.obscpio
autogits-tmp.tar.zst
*.osc
*.conf

15
_service Normal file
View File

@@ -0,0 +1,15 @@
<services>
<!-- workaround, go_modules needs a tar and obs_scm doesn't take file://. -->
<service name="roast" mode="manual">
<param name="target">.</param>
<param name="reproducible">true</param>
<param name="outfile">autogits-tmp.tar.zst</param>
<param name="exclude">autogits-tmp.tar.zst</param>
</service>
<service name="go_modules" mode="manual">
<param name="basename">./</param>
<param name="compression">zst</param>
<param name="vendorname">vendor</param>
</service>
</services>

View File

@@ -22,7 +22,6 @@ Release: 0
Summary: GitWorkflow utilities
License: GPL-2.0-or-later
URL: https://src.opensuse.org/adamm/autogits
BuildRequires: git
BuildRequires: systemd-rpm-macros
BuildRequires: go
%{?systemd_ordering}
@@ -32,85 +31,55 @@ Git Workflow tooling and utilities enabling automated handing of OBS projects
as git repositories
%package -n autogits-devel-importer
Summary: Imports devel projects from obs to git
%description -n autogits-devel-importer
Command-line tool to import devel projects from obs to git
%package -n autogits-doc
Summary: Common documentation files
%description -n autogits-doc
Common documentation files
%package -n autogits-hujson
Summary: HuJSON to JSON parser
%description -n autogits-hujson
HuJSON to JSON parser, using stdin -> stdout pipe
%package -n autogits-gitea-events-rabbitmq-publisher
%package -n gitea-events-rabbitmq-publisher
Summary: Publishes Gitea webhook data via RabbitMQ
%description -n autogits-gitea-events-rabbitmq-publisher
%description -n gitea-events-rabbitmq-publisher
Listens on an HTTP socket and publishes Gitea events on a RabbitMQ instance
with a topic
<scope>.src.$organization.$webhook_type.[$webhook_action_type]
%package -n autogits-gitea-status-proxy
Summary: gitea-status-proxy
%package -n doc
Summary: Common documentation files
%description -n autogits-gitea-status-proxy
%description -n doc
Common documentation files
%package -n autogits-group-review
%package -n group-review
Summary: Reviews of groups defined in ProjectGit
%description -n autogits-group-review
%description -n group-review
Is used to handle reviews associated with groups defined in the
ProjectGit.
%package -n autogits-obs-forward-bot
Summary: obs-forward-bot
%description -n autogits-obs-forward-bot
%package -n autogits-obs-staging-bot
%package -n obs-staging-bot
Summary: Build a PR against a ProjectGit, if review is requested
%description -n autogits-obs-staging-bot
%description -n obs-staging-bot
Build a PR against a ProjectGit, if review is requested.
%package -n autogits-obs-status-service
%package -n obs-status-service
Summary: Reports build status of OBS service as an easily to produce SVG
%description -n autogits-obs-status-service
%description -n obs-status-service
Reports build status of OBS service as an easily to produce SVG
%package -n autogits-workflow-direct
%package -n workflow-direct
Summary: Keep ProjectGit in sync for a devel project
Requires: openssh-clients
Requires: git-core
%description -n autogits-workflow-direct
%description -n workflow-direct
Keep ProjectGit in sync with packages in the organization of a devel project
%package -n autogits-workflow-pr
%package -n workflow-pr
Summary: Keeps ProjectGit PR in-sync with a PackageGit PR
Requires: openssh-clients
Requires: git-core
%description -n autogits-workflow-pr
%description -n workflow-pr
Keeps ProjectGit PR in-sync with a PackageGit PR
@@ -119,24 +88,12 @@ Keeps ProjectGit PR in-sync with a PackageGit PR
cp -r /home/abuild/rpmbuild/SOURCES/* ./
%build
go build \
-C devel-importer \
-buildmode=pie
go build \
-C hujson \
-buildmode=pie
go build \
-C gitea-events-rabbitmq-publisher \
-buildmode=pie
go build \
-C gitea_status_proxy \
-buildmode=pie
go build \
-C group-review \
-buildmode=pie
go build \
-C obs-forward-bot \
-buildmode=pie
go build \
-C obs-staging-bot \
-buildmode=pie
@@ -150,104 +107,59 @@ go build \
-C workflow-pr \
-buildmode=pie
%check
# TODO currently needs the source git history, maybe rewrite to create a git repo in the test?
#go test -C common
go test -C group-review
go test -C obs-staging-bot
go test -C obs-status-service
go test -C workflow-direct
# TODO build fails
#go test -C workflow-pr
%install
install -D -m0755 devel-importer/devel-importer %{buildroot}%{_bindir}/devel-importer
install -D -m0755 gitea-events-rabbitmq-publisher/gitea-events-rabbitmq-publisher %{buildroot}%{_bindir}/gitea-events-rabbitmq-publisher
install -D -m0644 systemd/gitea-events-rabbitmq-publisher.service %{buildroot}%{_unitdir}/gitea-events-rabbitmq-publisher.service
install -D -m0755 gitea_status_proxy/gitea_status_proxy %{buildroot}%{_bindir}/gitea_status_proxy
install -D -m0755 group-review/group-review %{buildroot}%{_bindir}/group-review
install -D -m0755 obs-forward-bot/obs-forward-bot %{buildroot}%{_bindir}/obs-forward-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 workflow-direct/workflow-direct %{buildroot}%{_bindir}/workflow-direct
install -D -m0755 workflow-pr/workflow-pr %{buildroot}%{_bindir}/workflow-pr
install -D -m0755 hujson/hujson %{buildroot}%{_bindir}/hujson
%pre -n autogits-gitea-events-rabbitmq-publisher
%pre -n gitea-events-rabbitmq-publisher
%service_add_pre gitea-events-rabbitmq-publisher.service
%post -n autogits-gitea-events-rabbitmq-publisher
%post -n gitea-events-rabbitmq-publisher
%service_add_post gitea-events-rabbitmq-publisher.service
%preun -n autogits-gitea-events-rabbitmq-publisher
%preun -n gitea-events-rabbitmq-publisher
%service_del_preun gitea-events-rabbitmq-publisher.service
%postun -n autogits-gitea-events-rabbitmq-publisher
%postun -n gitea-events-rabbitmq-publisher
%service_del_postun gitea-events-rabbitmq-publisher.service
%pre -n autogits-obs-staging-bot
%service_add_pre obs-staging-bot.service
%post -n autogits-obs-staging-bot
%service_add_post obs-staging-bot.service
%preun -n autogits-obs-staging-bot
%service_del_preun obs-staging-bot.service
%postun -n autogits-obs-staging-bot
%service_del_postun obs-staging-bot.service
%files -n autogits-devel-importer
%license COPYING
%doc devel-importer/README.md
%{_bindir}/devel-importer
%files -n autogits-hujson
%license COPYING
%{_bindir}/hujson
%files -n autogits-doc
%license COPYING
%doc doc/README.md
%doc doc/workflows.md
%files -n autogits-gitea-events-rabbitmq-publisher
%files -n gitea-events-rabbitmq-publisher
%license COPYING
%doc gitea-events-rabbitmq-publisher/README.md
%{_bindir}/gitea-events-rabbitmq-publisher
%{_unitdir}/gitea-events-rabbitmq-publisher.service
%files -n autogits-gitea-status-proxy
%files -n doc
%license COPYING
%{_bindir}/gitea_status_proxy
%doc doc/README.md
%doc doc/workflows.md
%files -n autogits-group-review
%files -n group-review
%license COPYING
%doc group-review/README.md
%{_bindir}/group-review
%files -n autogits-obs-forward-bot
%license COPYING
%{_bindir}/obs-forward-bot
%files -n autogits-obs-staging-bot
%files -n obs-staging-bot
%license COPYING
%doc obs-staging-bot/README.md
%{_bindir}/obs-staging-bot
%{_unitdir}/obs-staging-bot.service
%files -n autogits-obs-status-service
%files -n obs-status-service
%license COPYING
%doc obs-status-service/README.md
%{_bindir}/obs-status-service
%files -n autogits-workflow-direct
%files -n workflow-direct
%license COPYING
%doc workflow-direct/README.md
%{_bindir}/workflow-direct
%files -n autogits-workflow-pr
%files -n workflow-pr
%license COPYING
%doc workflow-pr/README.md
%{_bindir}/workflow-pr

View File

@@ -39,7 +39,7 @@ func parsePrLine(line string) (BasicPR, error) {
repo := strings.SplitN(org[1], "!", 2)
ret.Repo = repo[0]
if len(repo) != 2 {
repo = strings.SplitN(org[1], "#", 2)
repo := strings.SplitN(org[1], "#", 2)
ret.Repo = repo[0]
}
if len(repo) != 2 {

View File

@@ -34,7 +34,7 @@ func TestAssociatedPRScanner(t *testing.T) {
},
{
"Multiple PRs",
"Some header of the issue\n\nFollowed by some description\nPR: test/foo#4\n\nPR: test/goo!5\n",
"Some header of the issue\n\nFollowed by some description\nPR: test/foo#4\n\nPR: test/goo#5\n",
[]common.BasicPR{
{Org: "test", Repo: "foo", Num: 4},
{Org: "test", Repo: "goo", Num: 5},
@@ -107,7 +107,7 @@ func TestAppendingPRsToDescription(t *testing.T) {
[]common.BasicPR{
{Org: "a", Repo: "b", Num: 100},
},
"something\n\nPR: a/b!100",
"something\n\nPR: a/b#100",
},
{
"Append multiple PR to end of description",
@@ -119,7 +119,7 @@ func TestAppendingPRsToDescription(t *testing.T) {
{Org: "b", Repo: "b", Num: 100},
{Org: "c", Repo: "b", Num: 100},
},
"something\n\nPR: a1/b!100\nPR: a1/c!100\nPR: a1/c!101\nPR: b/b!100\nPR: c/b!100",
"something\n\nPR: a1/b#100\nPR: a1/c#100\nPR: a1/c#101\nPR: b/b#100\nPR: c/b#100",
},
{
"Append multiple sorted PR to end of description and remove dups",
@@ -133,7 +133,7 @@ func TestAppendingPRsToDescription(t *testing.T) {
{Org: "a1", Repo: "c", Num: 101},
{Org: "a1", Repo: "b", Num: 100},
},
"something\n\nPR: a1/b!100\nPR: a1/c!100\nPR: a1/c!101\nPR: b/b!100\nPR: c/b!100",
"something\n\nPR: a1/b#100\nPR: a1/c#100\nPR: a1/c#101\nPR: b/b#100\nPR: c/b#100",
},
}

View File

@@ -62,7 +62,6 @@ type AutogitConfig struct {
Committers []string // group in addition to Reviewers and Maintainers that can order the bot around, mostly as helper for factory-maintainers
Subdirs []string // list of directories to sort submodules into. Needed b/c _manifest cannot list non-existent directories
NoProjectGitPR bool // do not automatically create project git PRs, just assign reviewers and assume somethign else creates the ProjectGit PR
ManualMergeOnly bool // only merge with "Merge OK" comment by Project Maintainers and/or Package Maintainers and/or reviewers
ManualMergeProject bool // require merge of ProjectGit PRs with "Merge OK" by ProjectMaintainers and/or reviewers
}

View File

@@ -109,7 +109,6 @@ func TestConfigWorkflowParser(t *testing.T) {
}
}
// FIXME: should test ReadWorkflowConfig as it will always set prjgit completely
func TestProjectGitParser(t *testing.T) {
tests := []struct {
name string
@@ -120,21 +119,20 @@ func TestProjectGitParser(t *testing.T) {
}{
{
name: "repo only",
prjgit: "repo.git#master",
prjgit: "repo.git",
org: "org",
branch: "br",
res: [3]string{"org", "repo.git", "master"},
},
{
name: "default",
org: "org",
prjgit: "org/_ObsPrj#master",
res: [3]string{"org", common.DefaultGitPrj, "master"},
name: "default",
org: "org",
res: [3]string{"org", common.DefaultGitPrj, "master"},
},
{
name: "repo with branch",
org: "org2",
prjgit: "org2/repo.git#somebranch",
prjgit: "repo.git#somebranch",
res: [3]string{"org2", "repo.git", "somebranch"},
},
{
@@ -151,25 +149,25 @@ func TestProjectGitParser(t *testing.T) {
{
name: "repo org and empty branch",
org: "org3",
prjgit: "oorg/foo.bar#master",
prjgit: "oorg/foo.bar#",
res: [3]string{"oorg", "foo.bar", "master"},
},
{
name: "only branch defined",
org: "org3",
prjgit: "org3/_ObsPrj#mybranch",
prjgit: "#mybranch",
res: [3]string{"org3", "_ObsPrj", "mybranch"},
},
{
name: "only org and branch defined",
org: "org3",
prjgit: "org1/_ObsPrj#mybranch",
prjgit: "org1/#mybranch",
res: [3]string{"org1", "_ObsPrj", "mybranch"},
},
{
name: "empty org and repo",
org: "org3",
prjgit: "org3/repo#master",
prjgit: "/repo#",
res: [3]string{"org3", "repo", "master"},
},
}

View File

@@ -67,7 +67,6 @@ type Git interface {
GitExecOrPanic(cwd string, params ...string)
GitExec(cwd string, params ...string) error
GitExecWithOutput(cwd string, params ...string) (string, error)
GitExecQuietOrPanic(cwd string, params ...string)
GitDiffLister
}
@@ -77,8 +76,7 @@ type GitHandlerImpl struct {
GitCommiter string
GitEmail string
lock *sync.Mutex
quiet bool
lock *sync.Mutex
}
func (s *GitHandlerImpl) GetPath() string {
@@ -213,7 +211,7 @@ func (e *GitHandlerImpl) GitClone(repo, branch, remoteUrl string) (string, error
return "", fmt.Errorf("Cannot parse remote URL: %w", err)
}
remoteBranch := "HEAD"
if len(branch) == 0 && remoteUrlComp != nil && remoteUrlComp.Commit != "HEAD" {
if len(branch) == 0 && remoteUrlComp != nil {
branch = remoteUrlComp.Commit
remoteBranch = branch
} else if len(branch) > 0 {
@@ -242,12 +240,12 @@ func (e *GitHandlerImpl) GitClone(repo, branch, remoteUrl string) (string, error
// check if we have submodule to deinit
if list, _ := e.GitSubmoduleList(repo, "HEAD"); len(list) > 0 {
e.GitExecQuietOrPanic(repo, "submodule", "deinit", "--all", "--force")
e.GitExecOrPanic(repo, "submodule", "deinit", "--all", "--force")
}
e.GitExecOrPanic(repo, "fetch", "--prune", remoteName, remoteBranch)
}
/*
refsBytes, err := os.ReadFile(path.Join(e.GitPath, repo, ".git/refs/remotes", remoteName, "HEAD"))
if err != nil {
LogError("Cannot read HEAD of remote", remoteName)
@@ -266,7 +264,7 @@ func (e *GitHandlerImpl) GitClone(repo, branch, remoteUrl string) (string, error
LogDebug("remoteRef", remoteRef)
LogDebug("branch", branch)
}
*/
args := []string{"fetch", "--prune", remoteName, branch}
if strings.TrimSpace(e.GitExecWithOutputOrPanic(repo, "rev-parse", "--is-shallow-repository")) == "true" {
args = slices.Insert(args, 1, "--unshallow")
@@ -276,7 +274,7 @@ func (e *GitHandlerImpl) GitClone(repo, branch, remoteUrl string) (string, error
}
func (e *GitHandlerImpl) GitBranchHead(gitDir, branchName string) (string, error) {
id, err := e.GitExecWithOutput(gitDir, "show-ref", "--heads", "--hash", branchName)
id, err := e.GitExecWithOutput(gitDir, "show-ref", "--branches", "--hash", branchName)
if err != nil {
return "", fmt.Errorf("Can't find default branch: %s", branchName)
}
@@ -363,9 +361,7 @@ func (e *GitHandlerImpl) GitExecWithOutput(cwd string, params ...string) (string
LogDebug("git execute @", cwd, ":", cmd.Args)
out, err := cmd.CombinedOutput()
if !e.quiet {
LogDebug(string(out))
}
LogDebug(string(out))
if err != nil {
LogError("git", cmd.Args, " error:", err)
return "", fmt.Errorf("error executing: git %#v \n%s\n err: %w", cmd.Args, out, err)
@@ -374,13 +370,6 @@ func (e *GitHandlerImpl) GitExecWithOutput(cwd string, params ...string) (string
return string(out), nil
}
func (e *GitHandlerImpl) GitExecQuietOrPanic(cwd string, params ...string) {
e.quiet = true
e.GitExecOrPanic(cwd, params...)
e.quiet = false
return
}
type ChanIO struct {
ch chan byte
}

View File

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

View File

@@ -63,10 +63,6 @@ func SetLoggingLevel(ll LogLevel) {
logLevel = ll
}
func GetLoggingLevel() LogLevel {
return logLevel
}
func SetLoggingLevelFromString(ll string) error {
switch ll {
case "info":

View File

@@ -63,6 +63,7 @@ func readPRData(gitea GiteaPRFetcher, pr *models.PullRequest, currentSet []*PRIn
var Timeline_RefIssueNotFound error = errors.New("RefIssue not found on the timeline")
func LastPrjGitRefOnTimeline(gitea GiteaPRTimelineFetcher, org, repo string, num int64, prjGitOrg, prjGitRepo string) (*models.PullRequest, error) {
prRefLine := fmt.Sprintf(PrPattern, org, repo, num)
timeline, err := gitea.GetTimeline(org, repo, num)
if err != nil {
LogError("Failed to fetch timeline for", org, repo, "#", num, err)
@@ -78,9 +79,9 @@ func LastPrjGitRefOnTimeline(gitea GiteaPRTimelineFetcher, org, repo string, num
issue.Repository.Owner == prjGitOrg &&
issue.Repository.Name == prjGitRepo {
_, prs := ExtractDescriptionAndPRs(bufio.NewScanner(strings.NewReader(item.RefIssue.Body)))
for _, pr := range prs {
if pr.Org == org && pr.Repo == repo && pr.Num == num {
lines := SplitLines(item.RefIssue.Body)
for _, line := range lines {
if strings.TrimSpace(line) == prRefLine {
LogDebug("Found PrjGit PR in Timeline:", issue.Index)
// found prjgit PR in timeline. Return it
@@ -281,12 +282,6 @@ func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequester, maintaine
return nil
}
func (rs *PRSet) RemoveClosedPRs() {
rs.PRs = slices.DeleteFunc(rs.PRs, func(pr *PRInfo) bool {
return pr.PR.State != "open"
})
}
func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData) bool {
configReviewers := ParseReviewers(rs.Config.Reviewers)
@@ -385,8 +380,7 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
}
prjgit := prjgit_info.PR
_, _, prjgitBranch := rs.Config.GetPrjGit()
remote, err := git.GitClone(DefaultGitPrj, prjgitBranch, prjgit.Base.Repo.SSHURL)
remote, err := git.GitClone(DefaultGitPrj, rs.Config.Branch, prjgit.Base.Repo.SSHURL)
PanicOnError(err)
git.GitExecOrPanic(DefaultGitPrj, "fetch", remote, prjgit.Head.Sha)
@@ -415,7 +409,6 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
// we can only resolve conflicts with .gitmodules
for _, s := range status {
if s.Status == GitStatus_Unmerged {
panic("Can't handle conflicts yet")
if s.Path != ".gitmodules" {
return err
}
@@ -506,15 +499,7 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
if rs.IsPrjGitPR(prinfo.PR) {
continue
}
br := rs.Config.Branch
if len(br) == 0 {
// if branch is unspecified, take it from the PR as it
// matches default branch already
br = prinfo.PR.Base.Name
} else if br != prinfo.PR.Base.Name {
panic(prinfo.PR.Base.Name + " is expected to match " + br)
}
prinfo.RemoteName, err = git.GitClone(repo.Name, br, repo.SSHURL)
prinfo.RemoteName, err = git.GitClone(repo.Name, rs.Config.Branch, repo.SSHURL)
PanicOnError(err)
git.GitExecOrPanic(repo.Name, "fetch", prinfo.RemoteName, head.Sha)
git.GitExecOrPanic(repo.Name, "merge", "--ff", head.Sha)

View File

@@ -48,13 +48,11 @@ func reviewsToTimeline(reviews []*models.PullReview) []*models.TimelineComment {
}
func TestPR(t *testing.T) {
return
baseConfig := common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch",
Organization: "foo",
GitProjectName: "foo/barPrj#master",
GitProjectName: "barPrj",
}
type prdata struct {
@@ -640,7 +638,7 @@ func TestPRAssignReviewers(t *testing.T) {
{
name: "No reviewers",
config: common.AutogitConfig{
GitProjectName: "prg/repo#main",
GitProjectName: "repo",
Organization: "org",
Branch: "main",
Reviewers: []string{},
@@ -650,7 +648,7 @@ func TestPRAssignReviewers(t *testing.T) {
{
name: "One project reviewer only",
config: common.AutogitConfig{
GitProjectName: "prg/repo#main",
GitProjectName: "repo",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1"},
@@ -660,7 +658,7 @@ func TestPRAssignReviewers(t *testing.T) {
{
name: "One project reviewer and one pkg reviewer only",
config: common.AutogitConfig{
GitProjectName: "prg/repo#main",
GitProjectName: "repo",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1", "user2"},
@@ -670,7 +668,7 @@ func TestPRAssignReviewers(t *testing.T) {
{
name: "No need to get reviews of submitter",
config: common.AutogitConfig{
GitProjectName: "prg/repo#main",
GitProjectName: "repo",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1", "submitter"},
@@ -680,7 +678,7 @@ func TestPRAssignReviewers(t *testing.T) {
{
name: "Reviews are done",
config: common.AutogitConfig{
GitProjectName: "prg/repo#main",
GitProjectName: "repo",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1", "user2"},
@@ -714,7 +712,7 @@ func TestPRAssignReviewers(t *testing.T) {
{
name: "Stale review is not done, re-request it",
config: common.AutogitConfig{
GitProjectName: "org/repo#main",
GitProjectName: "repo",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1", "user2"},
@@ -746,7 +744,7 @@ func TestPRAssignReviewers(t *testing.T) {
{
name: "Stale optional review is not done, re-request it",
config: common.AutogitConfig{
GitProjectName: "prg/repo#main",
GitProjectName: "repo",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1", "user2", "~bot"},
@@ -854,7 +852,7 @@ func TestPRAssignReviewers(t *testing.T) {
{
name: "PrjMaintainers in prjgit review when not part of pkg set",
config: common.AutogitConfig{
GitProjectName: "org/repo#main",
GitProjectName: "repo",
Organization: "org",
Branch: "main",
Reviewers: []string{},
@@ -925,7 +923,7 @@ func TestPRMerge(t *testing.T) {
config := &common.AutogitConfig{
Organization: "org",
GitProjectName: "org/prj#master",
GitProjectName: "prj",
}
tests := []struct {

View File

@@ -164,7 +164,7 @@ func (l *RabbitConnection) ConnectAndProcessRabbitMQ(ch chan<- RabbitMessage) {
for {
err := l.ProcessRabbitMQ(ch)
if err != nil {
LogError("Error in RabbitMQ connection:", err)
LogError("Error in RabbitMQ connection. %#v", err)
LogInfo("Reconnecting in 2 seconds...")
time.Sleep(2 * time.Second)
}

View File

@@ -99,12 +99,12 @@ func (gitea *RabbitMQGiteaEventsProcessor) ProcessRabbitMessage(msg RabbitMessag
req, err := ParseRequestJSON(reqType, msg.Body)
if err != nil {
LogError("Error parsing request JSON:", err)
return nil
} else {
LogDebug("processing req", req.Type)
// h.Request = req
ProcessEvent(handler, req)
}
return nil
}
}

4
go.mod
View File

@@ -10,10 +10,8 @@ require (
github.com/go-openapi/validate v0.24.0
github.com/opentracing/opentracing-go v1.2.0
github.com/rabbitmq/amqp091-go v1.10.0
github.com/redis/go-redis/v9 v9.11.0
github.com/tailscale/hujson v0.0.0-20250226034555-ec1d1c113d33
go.uber.org/mock v0.5.0
gopkg.in/yaml.v3 v3.0.1
)
require (
@@ -32,9 +30,11 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/redis/go-redis/v9 v9.11.0 // indirect
go.mongodb.org/mongo-driver v1.14.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/sync v0.7.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

4
go.sum
View File

@@ -1,9 +1,5 @@
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View File

@@ -125,16 +125,6 @@ func FindAcceptableReviewInTimeline(user string, timeline []*models.TimelineComm
return nil
}
func FindOurLastReviewInTimeline(timeline []*models.TimelineComment) *models.TimelineComment {
for _, t := range timeline {
if t.Type == common.TimelineCommentType_Review && t.User.UserName == groupName && t.Created == t.Updated {
return t
}
}
return nil
}
func UnrequestReviews(gitea common.Gitea, org, repo string, id int64, users []string) {
if err := gitea.UnrequestReview(org, repo, id, users...); err != nil {
common.LogError("Can't remove reviewrs after a review:", err)
@@ -174,22 +164,6 @@ 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 {
@@ -199,32 +173,42 @@ func ProcessPR(pr *models.PullRequest) error {
}
if !found {
common.LogInfo(" review is not requested for", groupName)
return nil
if !common.IsDryRun {
gitea.SetNotificationRead(notification.ID)
}
return
}
config := configs.GetPrjGitConfig(org, repo, pr.Base.Name)
if config == nil {
return fmt.Errorf("Cannot find config for: %s", pr.URL)
common.LogError("Cannot find config for:", fmt.Sprintf("%s/%s!%s", org, repo, pr.Base.Name))
return
}
if pr.State == "closed" {
// dismiss the review
common.LogInfo(" -- closed request, so nothing to review")
return nil
if !common.IsDryRun {
gitea.SetNotificationRead(notification.ID)
}
return
}
reviews, err := gitea.GetPullRequestReviews(org, repo, id)
if err != nil {
return fmt.Errorf("Failed to fetch reviews for: %v: %w", pr.URL, err)
common.LogInfo(" ** No reviews associated with request:", subject.URL, "Error:", err)
return
}
timeline, err := common.FetchTimelineSinceReviewRequestOrPush(gitea, groupName, pr.Head.Sha, org, repo, id)
if err != nil {
return fmt.Errorf("Failed to fetch timeline to review. %w", err)
common.LogError(err)
return
}
groupConfig, err := config.GetReviewGroup(groupName)
if err != nil {
return fmt.Errorf("Failed to fetch review group. %w", err)
common.LogError(err)
return
}
// submitter cannot be reviewer
@@ -236,31 +220,27 @@ func ProcessPR(pr *models.PullRequest) error {
if review := FindAcceptableReviewInTimeline(reviewer, timeline, reviews); review != nil {
if ReviewAccepted(review.Body) {
if !common.IsDryRun {
text := reviewer + " approved a review on behalf of " + groupName
if review := FindOurLastReviewInTimeline(timeline); review == nil || review.Body != text {
_, err := gitea.AddReviewComment(pr, common.ReviewStateApproved, text)
if err != nil {
common.LogError(" -> failed to write approval comment", err)
gitea.AddReviewComment(pr, common.ReviewStateApproved, "Signed off by: "+reviewer)
UnrequestReviews(gitea, org, repo, id, requestReviewers)
if !common.IsDryRun {
if err := gitea.SetNotificationRead(notification.ID); err != nil {
common.LogDebug(" Cannot set notification as read", err)
}
UnrequestReviews(gitea, org, repo, id, requestReviewers)
}
}
common.LogInfo(" -> approved by", reviewer)
common.LogInfo(" review at", review.Created)
return nil
return
} else if ReviewRejected(review.Body) {
if !common.IsDryRun {
text := reviewer + " requested changes on behalf of " + groupName + ". See " + review.HTMLURL
if review := FindOurLastReviewInTimeline(timeline); review == nil || review.Body != text {
_, err := gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Changes requested. See review by: "+reviewer)
if err != nil {
common.LogError(" -> failed to write rejecting comment", err)
}
UnrequestReviews(gitea, org, repo, id, requestReviewers)
gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Changes requested. See review by: "+reviewer)
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 nil
return
}
}
}
@@ -290,22 +270,15 @@ func ProcessPR(pr *models.PullRequest) error {
}
if !found_help_comment && !common.IsDryRun {
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.")
helpComment := fmt.Sprintln("Review by", groupName, "represents a group of reviewers:", strings.Join(requestReviewers, ", "), ". To review as part of this group, create a comment with contents @"+groupName+": LGTM on a separate line to accept a review. To request changes, write @"+groupName+": followed by reason for rejection. Do not use reviews to review as a group. Editing a comment invalidates that comment.")
if slices.Contains(groupConfig.Reviewers, pr.User.UserName) {
helpComment = helpComment + "\n\n" +
"Submitter is member of this review group, hence they are excluded from being one of the reviewers here"
helpComment = helpComment + "\n\n" + fmt.Sprintln("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() {
func PeriodReviewCheck(gitea common.Gitea) {
notifications, err := gitea.GetNotifications(common.GiteaNotificationType_Pull, nil)
if err != nil {
common.LogError(" Error fetching unread notifications: %w", err)
@@ -314,15 +287,14 @@ func PeriodReviewCheck() {
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", 10, "Notification polling interval in minutes (min 1 min)")
interval := flag.Int64("interval", 5, "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")
@@ -359,7 +331,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)
@@ -403,13 +375,10 @@ 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_IssueComment: &process_issue_pr,
common.RequestType_Push: &config_update,
},
}
configUpdates.Connection().RabbitURL = u
@@ -446,7 +415,7 @@ func main() {
}
}
PeriodReviewCheck()
PeriodReviewCheck(gitea)
time.Sleep(time.Duration(*interval * int64(time.Minute)))
}
}

View File

@@ -7,25 +7,6 @@ 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

@@ -1,27 +0,0 @@
package main
import (
"io"
"log"
"os"
"github.com/tailscale/hujson"
)
func main() {
data, err := io.ReadAll(os.Stdin)
if err != nil {
log.Println(err)
os.Exit(1)
}
json, err := hujson.Standardize(data)
if err != nil {
log.Println(err)
os.Exit(2)
}
_, err = os.Stdout.Write(json)
if err != nil {
log.Print(err)
os.Exit(3)
}
}

View File

@@ -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, stagingMasterPrj string) (*common.ProjectMeta, error) {
func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingPrj, buildPrj string) (*common.ProjectMeta, error) {
common.LogDebug("repo content fetching ...")
err := FetchPrGit(git, pr)
if err != nil {
@@ -289,15 +289,7 @@ func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullReque
}
}
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)
}
meta, err := ObsClient.GetProjectMeta(buildPrj)
if err != nil {
common.LogError("error fetching project meta for", buildPrj, ". Err:", err)
return nil, err
@@ -322,13 +314,10 @@ func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullReque
urlPkg = append(urlPkg, "onlybuild="+url.QueryEscape(pkg))
}
meta.ScmSync = pr.Head.Repo.CloneURL + "?" + strings.Join(urlPkg, "&") + "#" + pr.Head.Sha
if len(meta.ScmSync) >= 65535 {
return nil, errors.New("Reached max amount of package changes per request")
}
meta.Title = fmt.Sprintf("PR#%d to %s", pr.Index, pr.Base.Name)
// QE wants it published ... also we should not hardcode it here, since
// it is configurable via the :PullRequest project
// meta.PublicFlags = common.Flags{Contents: "<disable/>"}
// QE wants it published ... also we should not hardcode it here, since
// it is configurable via the :PullRequest project
// meta.PublicFlags = common.Flags{Contents: "<disable/>"}
meta.Groups = nil
meta.Persons = nil
@@ -425,8 +414,7 @@ func StartOrUpdateBuild(config *common.StagingConfig, git common.Git, gitea comm
var state RequestModification = RequestModificationSourceChanged
if meta == nil {
// new build
common.LogDebug(" Staging master:", config.StagingProject)
meta, err = GenerateObsPrjMeta(git, gitea, pr, obsPrProject, config.ObsProject, config.StagingProject)
meta, err = GenerateObsPrjMeta(git, gitea, pr, obsPrProject, config.ObsProject)
if err != nil {
return RequestModificationNoChange, err
}
@@ -440,8 +428,6 @@ 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
}
@@ -598,7 +584,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.Subject.HTMLURL)
common.LogInfo("Cooldown period for cleanup of", thread.URL)
return false
}
@@ -636,14 +622,6 @@ func CleanupPullNotification(gitea common.Gitea, thread *models.NotificationThre
return false // cleaned up now, but the cleanup was not aleady done
}
func SetStatus(gitea common.Gitea, org, repo, hash string, status *models.CommitStatus) error {
_, err := gitea.SetCommitStatus(org, repo, hash, status)
if err != nil {
common.LogError(err)
}
return err
}
func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, error) {
dir, err := os.MkdirTemp(os.TempDir(), BotName)
common.PanicOnError(err)
@@ -827,12 +805,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
if !rebuild_all {
common.LogInfo("No package changes detected. Ignoring")
if !IsDryRun {
_, 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
}
_, err = gitea.AddReviewComment(pr, common.ReviewStateComment, "No package changes. Not rebuilding project by default")
}
return true, err
}
@@ -848,22 +821,6 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
TargetURL: ObsWebHost + "/project/show/" + stagingProject,
}
if err != nil {
msg := "Unable to setup stage project " + stagingConfig.ObsProject
status.Status = common.CommitStatus_Fail
common.LogError(msg)
if !IsDryRun {
SetStatus(gitea, org, repo, pr.Head.Sha, status)
_, err = gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, msg)
if err != nil {
common.LogError(err)
} else {
return true, nil
}
}
return false, nil
}
msg := "Changed source updated for build"
if change == RequestModificationProjectCreated {
msg = "Build is started in " + ObsWebHost + "/project/show/" +
@@ -872,7 +829,8 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
if len(stagingConfig.QA) > 0 {
msg = msg + "\nAdditional QA builds: \n"
}
SetStatus(gitea, org, repo, pr.Head.Sha, status)
gitea.SetCommitStatus(pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Head.Sha, status)
for _, setup := range stagingConfig.QA {
CreateQASubProject(stagingConfig, git, gitea, pr,
stagingProject,
@@ -896,34 +854,32 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
}
buildStatus := ProcessBuildStatus(stagingResult, baseResult)
done := false
switch buildStatus {
case BuildStatusSummarySuccess:
status.Status = common.CommitStatus_Success
done = true
if !IsDryRun {
_, err := gitea.AddReviewComment(pr, common.ReviewStateApproved, "Build successful")
if err != nil {
common.LogError(err)
} else {
return true, nil
}
}
case BuildStatusSummaryFailed:
status.Status = common.CommitStatus_Fail
done = true
if !IsDryRun {
_, err := gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Build failed")
if err != nil {
common.LogError(err)
} else {
return true, nil
}
}
}
common.LogInfo("Build status:", buildStatus)
if !IsDryRun {
if err = SetStatus(gitea, org, repo, pr.Head.Sha, status); err != nil {
return false, err
}
}
return done, nil
gitea.SetCommitStatus(pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Head.Sha, status)
// waiting for build results -- nothing to do
} else if err == NonActionableReviewError || err == NoReviewsFoundError {
return true, nil

View File

@@ -20,7 +20,6 @@ package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
@@ -192,24 +191,15 @@ func main() {
return
}
var rescanRepoError error
go func() {
for {
if rescanRepoError = RescanRepositories(); rescanRepoError != nil {
if err := RescanRepositories(); err != 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)
@@ -269,33 +259,6 @@ func main() {
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 {
res.WriteHeader(400)
return
}
packages := FindPackages(names[0])
data, err := json.MarshalIndent(packages, "", " ")
if err != nil {
res.WriteHeader(500)
common.LogError("Error in marshalling data.", err)
return
}
res.Write(data)
res.Header().Add("content-type", "application/json")
res.WriteHeader(200)
})
http.HandleFunc("GET /buildlog/{Project}/{Package}/{Repository}/{Arch}", func(res http.ResponseWriter, req *http.Request) {
prj := req.PathValue("Project")
pkg := req.PathValue("Package")

View File

@@ -29,15 +29,13 @@ func UpdateResults(r *common.BuildResult) {
RepoStatusLock.Lock()
defer RepoStatusLock.Unlock()
updateResultsWithoutLocking(r)
}
func updateResultsWithoutLocking(r *common.BuildResult) {
key := "result." + r.Project + "/" + r.Repository + "/" + r.Arch
common.LogDebug(" + Updating", key)
data, err := redisClient.HGetAll(context.Background(), key).Result()
if err != nil {
common.LogError("Failed fetching build results for", key, err)
}
common.LogDebug(" + Update size", len(data))
reset_time := time.Date(1000, 1, 1, 1, 1, 1, 1, time.Local)
for _, pkg := range r.Status {
@@ -112,27 +110,6 @@ func FindRepoResults(project, repo string) []*common.BuildResult {
return ret
}
func FindPackages(pkg string) []string {
RepoStatusLock.RLock()
defer RepoStatusLock.RUnlock()
data := make([]string, 0, 100)
for _, repo := range RepoStatus {
for _, status := range repo.Status {
if pkg == status.Package {
entry := repo.Project + "/" + pkg
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 {
res := FindProjectResults(project)
wg := &sync.WaitGroup{}
@@ -184,8 +161,6 @@ func RescanRepositories() error {
RepoStatusLock.Unlock()
var count int
projectsLooked := make([]string, 0, 10000)
for {
var data []string
data, cursor, err = redisClient.ScanType(ctx, cursor, "", 1000, "hash").Result()
@@ -194,7 +169,6 @@ func RescanRepositories() error {
return err
}
wg := &sync.WaitGroup{}
RepoStatusLock.Lock()
for _, repo := range data {
r := strings.Split(repo, "/")
@@ -206,28 +180,14 @@ func RescanRepositories() error {
Repository: r[1],
Arch: r[2],
}
var pos int
var found bool
if pos, found = slices.BinarySearchFunc(RepoStatus, d, common.BuildResultComp); found {
if pos, found := slices.BinarySearchFunc(RepoStatus, d, common.BuildResultComp); found {
RepoStatus[pos].Dirty = true
} else {
d.Dirty = true
RepoStatus = slices.Insert(RepoStatus, pos, d)
count++
}
// fetch all keys, one per non-maintenance/non-home: projects, for package search
if idx, found := slices.BinarySearch(projectsLooked, d.Project); !found && !strings.Contains(d.Project, ":Maintenance:") && (len(d.Project) < 5 || d.Project[0:5] != "home:") {
projectsLooked = slices.Insert(projectsLooked, idx, d.Project)
wg.Add(1)
go func(r *common.BuildResult) {
updateResultsWithoutLocking(r)
wg.Done()
}(RepoStatus[pos])
}
}
wg.Wait()
RepoStatusLock.Unlock()
if cursor == 0 {

View File

@@ -1,16 +0,0 @@
[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

@@ -1,4 +0,0 @@
package main
// exists only to import this for go.modules
import "go.uber.org/mock/mockgen/model"

1
vendor/modules.txt vendored
View File

@@ -132,7 +132,6 @@ go.opentelemetry.io/otel/trace/embedded
# go.uber.org/mock v0.5.0
## explicit; go 1.22
go.uber.org/mock/gomock
go.uber.org/mock/mockgen/model
# golang.org/x/sync v0.7.0
## explicit; go 1.18
golang.org/x/sync/errgroup

View File

@@ -33,7 +33,6 @@ JSON
* _ManualMergeOnly_: (true, false) only merge if "merge ok" comment/review by package or project maintainers or reviewers
* _ManualMergeProject_: (true, false) only merge if "merge ok" by project maintainers or reviewers
* _ReviewRequired_: (true, false) ignores that submitter is a maintainer and require a review from other maintainer IFF available
* _NoProjectGitPR_: (true, false) do not create PrjGit PRs, but still process reviews, etc.
NOTE: `-rm`, `-removed`, `-deleted` are all removed suffixes used to indicate current branch is a placeholder for previously existing package. These branches will be ignored by the bot, and if default, the package will be removed and will not be added to the project.
example:

View File

@@ -3,7 +3,6 @@ package main
//go:generate mockgen -source=pr_processor.go -destination=mock/pr_processor.go -typed
import (
"encoding/json"
"fmt"
"path"
"runtime/debug"
@@ -28,10 +27,6 @@ func PrjGitDescription(prset *common.PRSet) (title string, desc string) {
if prset.IsPrjGitPR(pr.PR) {
continue
}
if pr.PR.State != "open" {
// remove PRs that are not open from description
continue
}
org, repo, idx := pr.PRComponents()
title_refs = append(title_refs, repo)
@@ -74,7 +69,7 @@ func updateSubmoduleInPR(submodule, headSha string, git common.Git) {
// as long as we can update to newer one later, we are still OK
git.GitExec(common.DefaultGitPrj, "submodule", "update", "--init", "--checkout", "--depth", "1", submodule)
common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submodule), "fetch", "--depth", "1", "origin", headSha))
common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submodule), "checkout", "-f", headSha))
common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submodule), "checkout", headSha))
}
type PRProcessor struct {
@@ -104,10 +99,7 @@ func AllocatePRProcessor(req *models.PullRequest, configs common.AutogitConfigs)
return nil, fmt.Errorf("Cannot find config for PR")
}
if common.GetLoggingLevel() >= common.LogLevelDebug {
cjson, _ := json.Marshal(config)
common.LogDebug("found config:", string(cjson))
}
common.LogDebug("found config", config)
if config == nil {
common.LogError("Cannot find config for branch '%s'", req.Base.Ref)
return nil, fmt.Errorf("Cannot find config for branch '%s'", req.Base.Ref)
@@ -166,10 +158,10 @@ func (pr *PRProcessor) SetSubmodulesToMatchPRSet(prset *common.PRSet) error {
submodule_found = true
if id != prHead {
ref := fmt.Sprintf(common.PrPattern, org, repo, idx)
commitMsg := fmt.Sprintln("auto-created for", repo, "\n\nThis commit was autocreated by", GitAuthor, "\n\nreferencing PRs:\n", ref)
commitMsg := fmt.Sprintln("auto-created for", repo, "\n\nThis commit was autocreated by", GitAuthor, "referencing\n", ref)
if revert {
commitMsg = fmt.Sprintln("auto-created for", repo, "\n\nThis commit was autocreated by", GitAuthor, "\n\nremoving PRs:\n", ref)
commitMsg = fmt.Sprintln("auto-created for", repo, "\n\nThis commit was autocreated by", GitAuthor, "removing\n", ref)
}
updateSubmoduleInPR(submodulePath, prHead, git)
@@ -178,8 +170,6 @@ func (pr *PRProcessor) SetSubmodulesToMatchPRSet(prset *common.PRSet) error {
common.LogDebug("submodule", repo, " hash:", id, " -> ", prHead)
common.PanicOnError(err)
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "commit", "-a", "-m", commitMsg))
pr.PR.Head.Sha = id // update the prset
}
submodule_found = true
break
@@ -219,7 +209,7 @@ func (pr *PRProcessor) CreatePRjGitPR(prjGitPRbranch string, prset *common.PRSet
return err
}
if !common.IsDryRun && !pr.config.NoProjectGitPR {
if !common.IsDryRun {
if headCommit != newHeadCommit {
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "push", RemoteName, "+HEAD:"+prjGitPRbranch))
}
@@ -257,7 +247,7 @@ func (pr *PRProcessor) RebaseAndSkipSubmoduleCommits(prset *common.PRSet, branch
for _, s := range statuses {
if s.SubmoduleChanges != "S..." {
git.GitExecOrPanic(common.DefaultGitPrj, "rebase", "--abort")
return fmt.Errorf("Unexpected conflict in rebase. %v", s)
return fmt.Errorf("Unexpected conflict in rebase. %s", s)
}
}
conflict = git.GitExec(common.DefaultGitPrj, "rebase", "--skip")
@@ -275,12 +265,6 @@ func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
}
git := pr.git
if len(prset.PRs) == 1 {
git.GitExecOrPanic(common.DefaultGitPrj, "fetch", PrjGitPR.RemoteName, PrjGitPR.PR.Head.Sha)
common.LogDebug("Only project git in PR. Nothing to update.")
return nil
}
PrjGit := PrjGitPR.PR.Base.Repo
prjGitPRbranch := PrjGitPR.PR.Head.Name
if strings.Contains(prjGitPRbranch, "/") {
@@ -316,17 +300,6 @@ func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
return err
}
PrjGitTitle, PrjGitBody := PrjGitDescription(prset)
if PrjGitPR.PR.User.UserName == CurrentUser.UserName {
if PrjGitPR.PR.Title != PrjGitTitle || PrjGitPR.PR.Body != PrjGitBody {
common.LogDebug("New title:", PrjGitTitle)
common.LogDebug(PrjGitBody)
}
} else {
// TODO: find our first comment in timeline
}
if !common.IsDryRun {
if headCommit != newHeadCommit {
params := []string{"push", PrjGitPR.RemoteName, "+HEAD:" + prjGitPRbranch}
@@ -337,6 +310,7 @@ func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
}
// update PR
PrjGitTitle, PrjGitBody := PrjGitDescription(prset)
if PrjGitPR.PR.Body != PrjGitBody || PrjGitPR.PR.Title != PrjGitTitle {
Gitea.UpdatePullRequest(PrjGit.Owner.UserName, PrjGit.Name, PrjGitPR.PR.Index, &models.EditPullRequestOption{
RemoveDeadline: true,
@@ -345,9 +319,6 @@ func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
})
}
}
// remove closed PRs from prset
prset.RemoveClosedPRs()
return nil
}
@@ -373,7 +344,7 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
prjGitPRbranch := prGitBranchNameForPR(prRepo, prNo)
prjGitPR, err := prset.GetPrjGitPR()
if err == common.PRSet_PrjGitMissing {
common.LogDebug("Missing PrjGit. Need to create one under branch", prjGitPRbranch)
common.LogDebug("Missing PrjGit. Need to create one...")
if err = pr.CreatePRjGitPR(prjGitPRbranch, prset); err != nil {
return err
@@ -450,11 +421,7 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
if prjGitPR == nil {
prjGitPR, err = prset.GetPrjGitPR()
if err == common.PRSet_PrjGitMissing && config.NoProjectGitPR {
// we could be waiting for other tooling to create the
// project git PR. In meantime, we can assign some
// reviewers here.
} else if err != nil {
if err != nil {
common.LogError("Error fetching PrjGitPR:", err)
return nil
}
@@ -466,12 +433,11 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
// reset anything that changed that is not part of the prset
// package removals/additions are *not* counted here
org, repo, branch := config.GetPrjGit()
// TODO: this is broken...
if pr, err := prset.GetPrjGitPR(); err == nil && false {
if pr, err := prset.GetPrjGitPR(); err == nil {
common.LogDebug("Submodule parse begin")
orig_subs, err := git.GitSubmoduleList(common.DefaultGitPrj, pr.PR.MergeBase)
common.PanicOnError(err)
new_subs, err := git.GitSubmoduleList(common.DefaultGitPrj, "HEAD")
new_subs, err := git.GitSubmoduleList(common.DefaultGitPrj, pr.PR.Head.Sha)
common.PanicOnError(err)
common.LogDebug("Submodule parse done")
@@ -479,7 +445,6 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
updateSubmoduleInPR(submodule, sha, git)
}
common.LogDebug("Checking we only change linked commits")
for path, commit := range new_subs {
if old, ok := orig_subs[path]; ok && old != commit {
found := false
@@ -487,8 +452,6 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
if pr.PR.Base.Repo.Name == path && commit == pr.PR.Head.Sha {
found = true
break
} else if pr.PR.Base.Repo.Name == path {
common.LogError(path, "-- commits not match", commit, pr.PR.Head.Sha)
}
}
if !found {
@@ -498,29 +461,24 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
}
stats, err := git.GitStatus(common.DefaultGitPrj)
common.LogDebug("Check Done", len(stats), "changes")
common.PanicOnError(err)
if len(stats) > 0 {
git.GitExecOrPanic(common.DefaultGitPrj, "commit", "-a", "-m", "Sync submodule updates with PR-set")
git.GitExecQuietOrPanic(common.DefaultGitPrj, "submodule", "deinit", "--all", "--force")
git.GitExecOrPanic(common.DefaultGitPrj, "submodule", "deinit", "--all", "--force")
if !common.IsDryRun {
git.GitExecOrPanic(common.DefaultGitPrj, "push")
}
}
}
if prjGitPR != nil {
common.LogDebug(" num of reviewers:", len(prjGitPR.PR.RequestedReviewers))
} else {
common.LogInfo("* No prjgit")
}
common.LogDebug(" num of reviewers:", len(prjGitPR.PR.RequestedReviewers))
maintainers, err := common.FetchProjectMaintainershipData(Gitea, org, repo, branch)
if err != nil {
return err
}
// handle case where PrjGit PR is only one left and there are no changes, then we can just close the PR
if len(prset.PRs) == 1 && prjGitPR != nil && prset.PRs[0] == prjGitPR && prjGitPR.PR.User.UserName == prset.BotUser {
if len(prset.PRs) == 1 && prset.PRs[0] == prjGitPR && prjGitPR.PR.User.UserName == prset.BotUser {
common.LogDebug(" --> checking if superflous PR")
diff, err := git.GitDiff(common.DefaultGitPrj, prjGitPR.PR.MergeBase, prjGitPR.PR.Head.Sha)
if err != nil {
@@ -542,6 +500,7 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
common.LogDebug(" --> NOT superflous PR")
}
prset.AssignReviewers(Gitea, maintainers)
for _, pr := range prset.PRs {
if err := verifyRepositoryConfiguration(pr.PR.Base.Repo); err != nil {
common.LogError("Cannot set manual merge... aborting processing")
@@ -556,9 +515,8 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
if err = prset.Merge(Gitea, git); err != nil {
common.LogError("merge error:", err)
}
} else {
prset.AssignReviewers(Gitea, maintainers)
}
return err
}