Compare commits

..

10 Commits

Author SHA256 Message Date
3112ed7f97 Use our IBS branch for packaging
Some checks failed
go-generate-check / go-generate-check (pull_request) Successful in 38s
Integration tests / t (pull_request) Failing after 9m40s
Need to use it from internal resource policy wise, but will be mirrored outside.
2026-02-26 11:12:30 +01:00
4dd54b5db1 staging: Hardcode currently used user of IBS
We should consolidate to a nicer user name...
2026-02-26 11:12:29 +01:00
dde0e04a03 Always handle build results as building when dirty flag is set 2026-02-26 11:12:29 +01:00
346c6d836a staging: Fix service file to work with obs ssh connections
We need a fixed user for that
2026-02-26 11:12:29 +01:00
cc37fa73bd Add an "lfs fsck" check after submodule update
to avoid merging pull requests where lfs objects are not correctly
registered. Can happen when user has not installed lfs for example.
2026-02-26 11:12:29 +01:00
7740021192 Disable temporary comment adding in case of lacking permissions 2026-02-26 11:12:29 +01:00
6b76c3efc0 pr: only update PR if elided title not changed
Gitea trims long titles so we need to compare if the trimmed length
is same, not entire string that will always differ.
2026-02-26 11:12:29 +01:00
d04b67fde7 Support remote source in pull requests
This requires write permission by maintainer there
2026-02-26 11:12:29 +01:00
93812e34d9 workflow-direct: use relative path when adding a submodule
This solves the issue of using the right credentials based on the
main repo.
Also it allows to rename the organisation.
2026-02-26 11:12:29 +01:00
439d1baf64 Fix build status check when package source is in subdir
We must not use the upper directory name as part of the package name.
2026-02-26 11:12:29 +01:00
19 changed files with 192 additions and 2091 deletions

View File

@@ -22,6 +22,8 @@ Release: 0
Summary: GitWorkflow utilities
License: GPL-2.0-or-later
URL: https://src.opensuse.org/adamm/autogits
#!RemoteAsset: git+https://src.suse.de/adrianSuSE/autogits#ibs_state
Source0: %name-%version.tar.xz
BuildRequires: git
BuildRequires: systemd-rpm-macros
BuildRequires: go

View File

@@ -288,7 +288,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)
if err != nil {
return "", fmt.Errorf("Can't find default branch: %s", branchName)
return "", fmt.Errorf("Can't find default branch: %s in %s", branchName, gitDir)
}
id = strings.TrimSpace(SplitLines(id)[0])
@@ -302,7 +302,7 @@ func (e *GitHandlerImpl) GitBranchHead(gitDir, branchName string) (string, error
func (e *GitHandlerImpl) GitRemoteHead(gitDir, remote, branchName string) (string, error) {
id, err := e.GitExecWithOutput(gitDir, "show-ref", "--hash", "--verify", "refs/remotes/"+remote+"/"+branchName)
if err != nil {
return "", fmt.Errorf("Can't find default branch: %s", branchName)
return "", fmt.Errorf("Can't find default branch: %s in %s", branchName, gitDir)
}
return strings.TrimSpace(id), nil

View File

@@ -83,260 +83,3 @@ func (c *MockObsStatusFetcherWithStateBuildStatusWithStateCall) DoAndReturn(f fu
c.Call = c.Call.DoAndReturn(f)
return c
}
// MockObsClientInterface is a mock of ObsClientInterface interface.
type MockObsClientInterface struct {
ctrl *gomock.Controller
recorder *MockObsClientInterfaceMockRecorder
isgomock struct{}
}
// MockObsClientInterfaceMockRecorder is the mock recorder for MockObsClientInterface.
type MockObsClientInterfaceMockRecorder struct {
mock *MockObsClientInterface
}
// NewMockObsClientInterface creates a new mock instance.
func NewMockObsClientInterface(ctrl *gomock.Controller) *MockObsClientInterface {
mock := &MockObsClientInterface{ctrl: ctrl}
mock.recorder = &MockObsClientInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockObsClientInterface) EXPECT() *MockObsClientInterfaceMockRecorder {
return m.recorder
}
// BuildStatus mocks base method.
func (m *MockObsClientInterface) BuildStatus(project string, packages ...string) (*common.BuildResultList, error) {
m.ctrl.T.Helper()
varargs := []any{project}
for _, a := range packages {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BuildStatus", varargs...)
ret0, _ := ret[0].(*common.BuildResultList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// BuildStatus indicates an expected call of BuildStatus.
func (mr *MockObsClientInterfaceMockRecorder) BuildStatus(project any, packages ...any) *MockObsClientInterfaceBuildStatusCall {
mr.mock.ctrl.T.Helper()
varargs := append([]any{project}, packages...)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildStatus", reflect.TypeOf((*MockObsClientInterface)(nil).BuildStatus), varargs...)
return &MockObsClientInterfaceBuildStatusCall{Call: call}
}
// MockObsClientInterfaceBuildStatusCall wrap *gomock.Call
type MockObsClientInterfaceBuildStatusCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockObsClientInterfaceBuildStatusCall) Return(arg0 *common.BuildResultList, arg1 error) *MockObsClientInterfaceBuildStatusCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockObsClientInterfaceBuildStatusCall) Do(f func(string, ...string) (*common.BuildResultList, error)) *MockObsClientInterfaceBuildStatusCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockObsClientInterfaceBuildStatusCall) DoAndReturn(f func(string, ...string) (*common.BuildResultList, error)) *MockObsClientInterfaceBuildStatusCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// DeleteProject mocks base method.
func (m *MockObsClientInterface) DeleteProject(project string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteProject", project)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteProject indicates an expected call of DeleteProject.
func (mr *MockObsClientInterfaceMockRecorder) DeleteProject(project any) *MockObsClientInterfaceDeleteProjectCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteProject", reflect.TypeOf((*MockObsClientInterface)(nil).DeleteProject), project)
return &MockObsClientInterfaceDeleteProjectCall{Call: call}
}
// MockObsClientInterfaceDeleteProjectCall wrap *gomock.Call
type MockObsClientInterfaceDeleteProjectCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockObsClientInterfaceDeleteProjectCall) Return(arg0 error) *MockObsClientInterfaceDeleteProjectCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockObsClientInterfaceDeleteProjectCall) Do(f func(string) error) *MockObsClientInterfaceDeleteProjectCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockObsClientInterfaceDeleteProjectCall) DoAndReturn(f func(string) error) *MockObsClientInterfaceDeleteProjectCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// GetHomeProject mocks base method.
func (m *MockObsClientInterface) GetHomeProject() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetHomeProject")
ret0, _ := ret[0].(string)
return ret0
}
// GetHomeProject indicates an expected call of GetHomeProject.
func (mr *MockObsClientInterfaceMockRecorder) GetHomeProject() *MockObsClientInterfaceGetHomeProjectCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHomeProject", reflect.TypeOf((*MockObsClientInterface)(nil).GetHomeProject))
return &MockObsClientInterfaceGetHomeProjectCall{Call: call}
}
// MockObsClientInterfaceGetHomeProjectCall wrap *gomock.Call
type MockObsClientInterfaceGetHomeProjectCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockObsClientInterfaceGetHomeProjectCall) Return(arg0 string) *MockObsClientInterfaceGetHomeProjectCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockObsClientInterfaceGetHomeProjectCall) Do(f func() string) *MockObsClientInterfaceGetHomeProjectCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockObsClientInterfaceGetHomeProjectCall) DoAndReturn(f func() string) *MockObsClientInterfaceGetHomeProjectCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// GetProjectMeta mocks base method.
func (m *MockObsClientInterface) GetProjectMeta(project string) (*common.ProjectMeta, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetProjectMeta", project)
ret0, _ := ret[0].(*common.ProjectMeta)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetProjectMeta indicates an expected call of GetProjectMeta.
func (mr *MockObsClientInterfaceMockRecorder) GetProjectMeta(project any) *MockObsClientInterfaceGetProjectMetaCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProjectMeta", reflect.TypeOf((*MockObsClientInterface)(nil).GetProjectMeta), project)
return &MockObsClientInterfaceGetProjectMetaCall{Call: call}
}
// MockObsClientInterfaceGetProjectMetaCall wrap *gomock.Call
type MockObsClientInterfaceGetProjectMetaCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockObsClientInterfaceGetProjectMetaCall) Return(arg0 *common.ProjectMeta, arg1 error) *MockObsClientInterfaceGetProjectMetaCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockObsClientInterfaceGetProjectMetaCall) Do(f func(string) (*common.ProjectMeta, error)) *MockObsClientInterfaceGetProjectMetaCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockObsClientInterfaceGetProjectMetaCall) DoAndReturn(f func(string) (*common.ProjectMeta, error)) *MockObsClientInterfaceGetProjectMetaCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// SetHomeProject mocks base method.
func (m *MockObsClientInterface) SetHomeProject(project string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetHomeProject", project)
}
// SetHomeProject indicates an expected call of SetHomeProject.
func (mr *MockObsClientInterfaceMockRecorder) SetHomeProject(project any) *MockObsClientInterfaceSetHomeProjectCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetHomeProject", reflect.TypeOf((*MockObsClientInterface)(nil).SetHomeProject), project)
return &MockObsClientInterfaceSetHomeProjectCall{Call: call}
}
// MockObsClientInterfaceSetHomeProjectCall wrap *gomock.Call
type MockObsClientInterfaceSetHomeProjectCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockObsClientInterfaceSetHomeProjectCall) Return() *MockObsClientInterfaceSetHomeProjectCall {
c.Call = c.Call.Return()
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockObsClientInterfaceSetHomeProjectCall) Do(f func(string)) *MockObsClientInterfaceSetHomeProjectCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockObsClientInterfaceSetHomeProjectCall) DoAndReturn(f func(string)) *MockObsClientInterfaceSetHomeProjectCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// SetProjectMeta mocks base method.
func (m *MockObsClientInterface) SetProjectMeta(meta *common.ProjectMeta) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetProjectMeta", meta)
ret0, _ := ret[0].(error)
return ret0
}
// SetProjectMeta indicates an expected call of SetProjectMeta.
func (mr *MockObsClientInterfaceMockRecorder) SetProjectMeta(meta any) *MockObsClientInterfaceSetProjectMetaCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProjectMeta", reflect.TypeOf((*MockObsClientInterface)(nil).SetProjectMeta), meta)
return &MockObsClientInterfaceSetProjectMetaCall{Call: call}
}
// MockObsClientInterfaceSetProjectMetaCall wrap *gomock.Call
type MockObsClientInterfaceSetProjectMetaCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockObsClientInterfaceSetProjectMetaCall) Return(arg0 error) *MockObsClientInterfaceSetProjectMetaCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockObsClientInterfaceSetProjectMetaCall) Do(f func(*common.ProjectMeta) error) *MockObsClientInterfaceSetProjectMetaCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockObsClientInterfaceSetProjectMetaCall) DoAndReturn(f func(*common.ProjectMeta) error) *MockObsClientInterfaceSetProjectMetaCall {
c.Call = c.Call.DoAndReturn(f)
return c
}

View File

@@ -46,15 +46,6 @@ type ObsStatusFetcherWithState interface {
BuildStatusWithState(project string, opts *BuildResultOptions, packages ...string) (*BuildResultList, error)
}
type ObsClientInterface interface {
GetProjectMeta(project string) (*ProjectMeta, error)
SetProjectMeta(meta *ProjectMeta) error
DeleteProject(project string) error
BuildStatus(project string, packages ...string) (*BuildResultList, error)
GetHomeProject() string
SetHomeProject(project string)
}
type ObsClient struct {
baseUrl *url.URL
client *http.Client
@@ -66,14 +57,6 @@ type ObsClient struct {
HomeProject string
}
func (c *ObsClient) GetHomeProject() string {
return c.HomeProject
}
func (c *ObsClient) SetHomeProject(project string) {
c.HomeProject = project
}
func NewObsClient(host string) (*ObsClient, error) {
baseUrl, err := url.Parse(host)
if err != nil {
@@ -711,13 +694,15 @@ func (r *BuildResultList) BuildResultSummary() (success, finished bool) {
if !ok {
panic("Unknown result code: " + result.Code)
}
if r.isLastBuild && result.Code == "unknown" {
// it means the package has never build yet,
// but we don't know the reason
detail.Finished = true
if r.isLastBuild {
// we are always finished, since it is the last result
// also when there is "unknown" state, it just means it
// it was never done
finished = true
} else {
finished = finished && detail.Finished
}
finished = finished && detail.Finished
success = success && detail.Success
if !finished {

View File

@@ -468,7 +468,7 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
LogError("Cannot fetch gita reaviews for PR:", err)
return false
}
r.SetRequiredReviewers(reviewers)
r.RequestedReviewers = reviewers
prjgit.Reviews = r
if prjgit.Reviews.IsManualMergeOK() {
is_manually_reviewed_ok = true
@@ -489,7 +489,7 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
LogError("Cannot fetch gita reaviews for PR:", err)
return false
}
r.SetRequiredReviewers(reviewers)
r.RequestedReviewers = reviewers
pr.Reviews = r
if !pr.Reviews.IsManualMergeOK() {
LogInfo("Not approved manual merge. PR:", pr.PR.URL)
@@ -530,7 +530,7 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
LogError("Cannot fetch gitea reaviews for PR:", err)
return false
}
r.SetRequiredReviewers(reviewers)
r.RequestedReviewers = reviewers
is_manually_reviewed_ok = r.IsApproved()
LogDebug("PR to", pr.PR.Base.Repo.Name, "reviewed?", is_manually_reviewed_ok)

View File

@@ -807,8 +807,9 @@ func TestFindMissingAndExtraReviewers(t *testing.T) {
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
},
Reviews: &common.PRReviews{
Reviews: []*models.PullReview{{State: common.ReviewStateRequestReview, User: &models.User{UserName: "m1"}}},
RequestedReviewers: []*models.TimelineComment{
Reviews: []*models.PullReview{{State: common.ReviewStateRequestReview, User: &models.User{UserName: "m1"}}},
RequestedReviewers: []string{"m1"},
FullTimeline: []*models.TimelineComment{
{User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "m1"}, Type: common.TimelineCommentType_ReviewRequested},
},
},
@@ -918,7 +919,8 @@ func TestFindMissingAndExtraReviewers(t *testing.T) {
},
Reviews: &common.PRReviews{
Reviews: []*models.PullReview{{State: common.ReviewStateRequestReview, User: &models.User{UserName: "reviewer"}}},
RequestedReviewers: []*models.TimelineComment{{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "reviewer"}}},
RequestedReviewers: []string{"reviewer"},
FullTimeline: []*models.TimelineComment{{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "reviewer"}}},
},
},
{
@@ -928,7 +930,8 @@ func TestFindMissingAndExtraReviewers(t *testing.T) {
},
Reviews: &common.PRReviews{
Reviews: []*models.PullReview{{State: common.ReviewStateRequestReview, User: &models.User{UserName: "reviewer"}}},
RequestedReviewers: []*models.TimelineComment{{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "reviewer"}}},
RequestedReviewers: []string{"reviewer"},
FullTimeline: []*models.TimelineComment{{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "reviewer"}}},
},
},
},
@@ -963,7 +966,8 @@ func TestFindMissingAndExtraReviewers(t *testing.T) {
{State: common.ReviewStateApproved, User: &models.User{UserName: "pkgmaintainer"}},
{State: common.ReviewStatePending, User: &models.User{UserName: "prjmaintainer"}},
},
RequestedReviewers: []*models.TimelineComment{
RequestedReviewers: []string{"user2", "pkgmaintainer", "prjmaintainer"},
FullTimeline: []*models.TimelineComment{
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "user2"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgmaintainer"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prjmaintainer"}},
@@ -981,7 +985,8 @@ func TestFindMissingAndExtraReviewers(t *testing.T) {
{State: common.ReviewStateRequestChanges, User: &models.User{UserName: "user1"}},
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "autogits_obs_staging_bot"}},
},
RequestedReviewers: []*models.TimelineComment{
RequestedReviewers: []string{"user1", "autogits_obs_staging_bot"},
FullTimeline: []*models.TimelineComment{
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "user1"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "autogits_obs_staging_bot"}},
},
@@ -1021,7 +1026,8 @@ func TestFindMissingAndExtraReviewers(t *testing.T) {
{State: common.ReviewStatePending, User: &models.User{UserName: "prj2"}},
{State: common.ReviewStatePending, User: &models.User{UserName: "someother"}},
},
RequestedReviewers: []*models.TimelineComment{
RequestedReviewers: []string{"user2", "pkgmaintainer", "prjmaintainer", "pkgm1", "pkgm2", "someother", "prj1", "prj2"},
FullTimeline: []*models.TimelineComment{
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgmaintainer"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prjmaintainer"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj1"}},
@@ -1044,7 +1050,8 @@ func TestFindMissingAndExtraReviewers(t *testing.T) {
{State: common.ReviewStatePending, User: &models.User{UserName: "prj1"}},
{State: common.ReviewStatePending, User: &models.User{UserName: "prj2"}},
},
RequestedReviewers: []*models.TimelineComment{
RequestedReviewers: []string{"user1", "autogits_obs_staging_bot", "prj1", "prj2"},
FullTimeline: []*models.TimelineComment{
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "autogits_obs_staging_bot"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj1"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj2"}},
@@ -1083,7 +1090,8 @@ func TestFindMissingAndExtraReviewers(t *testing.T) {
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "prj1"}},
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "someother"}},
},
RequestedReviewers: []*models.TimelineComment{
RequestedReviewers: []string{"user2", "pkgmaintainer", "prjmaintainer", "pkgm1", "someother", "prj1"},
FullTimeline: []*models.TimelineComment{
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgm1"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgmaintainer"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prjmaintainer"}},
@@ -1104,7 +1112,8 @@ func TestFindMissingAndExtraReviewers(t *testing.T) {
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "autogits_obs_staging_bot"}},
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "prj1"}},
},
RequestedReviewers: []*models.TimelineComment{
RequestedReviewers: []string{"user1", "autogits_obs_staging_bot", "prj1"},
FullTimeline: []*models.TimelineComment{
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "autogits_obs_staging_bot"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj1"}},
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "!bot"}, Assignee: &models.User{UserName: "user1"}},
@@ -1190,9 +1199,6 @@ func TestFindMissingAndExtraReviewers(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
test.prset.HasAutoStaging = !test.noAutoStaging
for idx, pr := range test.prset.PRs {
if pr.Reviews != nil {
pr.Reviews.SetRequiredReviewers(test.prset.Config.Reviewers)
}
missing, extra := test.prset.FindMissingAndExtraReviewers(test.maintainers, idx)
// avoid nil dereference below, by adding empty array elements

View File

@@ -8,28 +8,12 @@ import (
"src.opensuse.org/autogits/common/gitea-generated/models"
)
type ReviewInterface interface {
IsManualMergeOK() bool
IsApproved() bool
MisingReviews() []string
FindReviewRequester(reviewer string) *models.TimelineComment
HasPendingReviewBy(reviewer string) bool
IsReviewedBy(reviewer string) bool
IsReviewedByOneOf(reviewers ...string) bool
SetRequiredReviewers(reviewers []string)
}
type PRReviews struct {
Reviews []*models.PullReview
RequestedReviewers []*models.TimelineComment
RequestedReviewers []string
Comments []*models.TimelineComment
RequiredReviewers []string
}
func (r *PRReviews) SetRequiredReviewers(reviewers []string) {
r.RequiredReviewers = reviewers
FullTimeline []*models.TimelineComment
}
func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, org, repo string, no int64) (*PRReviews, error) {
@@ -44,11 +28,11 @@ func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, org, repo string, no int64
}
reviews := make([]*models.PullReview, 0, 10)
needNewReviews := []string{}
var comments []*models.TimelineComment
var foundUsers []string
alreadyHaveUserReview := func(user string) bool {
if slices.Contains(foundUsers, user) {
if slices.Contains(needNewReviews, user) {
return true
}
for _, r := range reviews {
@@ -64,24 +48,20 @@ func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, org, repo string, no int64
LogDebug("Number of items in timeline:", len(timeline))
cutOffIdx := len(timeline)
var PendingRequestedReviews []*models.TimelineComment
for idx, item := range timeline {
if item.Type == TimelineCommentType_Review {
if item.Type == TimelineCommentType_Review || item.Type == TimelineCommentType_ReviewRequested {
for _, r := range rawReviews {
if r.ID == item.ReviewID && r.User != nil {
if r.ID == item.ReviewID {
if !alreadyHaveUserReview(r.User.UserName) {
if idx < cutOffIdx {
if item.Type == TimelineCommentType_Review && idx > cutOffIdx {
needNewReviews = append(needNewReviews, r.User.UserName)
} else {
reviews = append(reviews, r)
}
foundUsers = append(foundUsers, r.User.UserName)
}
break
}
}
} else if item.Type == TimelineCommentType_ReviewRequested && item.Assignee != nil && !alreadyHaveUserReview(item.Assignee.UserName) {
PendingRequestedReviews = append(PendingRequestedReviews, item)
} else if item.Type == TimelineCommentType_DismissReview && item.Assignee != nil && !alreadyHaveUserReview(item.Assignee.UserName) {
foundUsers = append(foundUsers, item.Assignee.UserName)
} else if item.Type == TimelineCommentType_Comment && cutOffIdx > idx {
comments = append(comments, item)
} else if item.Type == TimelineCommentType_PushPull && cutOffIdx == len(timeline) {
@@ -94,9 +74,9 @@ func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, org, repo string, no int64
LogDebug("num comments:", len(comments), "timeline:", len(reviews))
return &PRReviews{
Reviews: reviews,
Comments: comments,
RequestedReviewers: PendingRequestedReviews,
Reviews: reviews,
Comments: comments,
FullTimeline: timeline,
}, nil
}
@@ -124,7 +104,7 @@ func (r *PRReviews) IsManualMergeOK() bool {
continue
}
LogDebug("comment:", c.User.UserName, c.Body)
if slices.Contains(r.RequiredReviewers, c.User.UserName) {
if slices.Contains(r.RequestedReviewers, c.User.UserName) {
if bodyCommandManualMergeOK(c.Body) {
return true
}
@@ -135,7 +115,7 @@ func (r *PRReviews) IsManualMergeOK() bool {
if c.Updated != c.Submitted {
continue
}
if slices.Contains(r.RequiredReviewers, c.User.UserName) {
if slices.Contains(r.RequestedReviewers, c.User.UserName) {
if bodyCommandManualMergeOK(c.Body) {
return true
}
@@ -151,7 +131,7 @@ func (r *PRReviews) IsApproved() bool {
}
goodReview := true
for _, reviewer := range r.RequiredReviewers {
for _, reviewer := range r.RequestedReviewers {
goodReview = false
for _, review := range r.Reviews {
if review.User.UserName == reviewer && review.State == ReviewStateApproved && !review.Stale && !review.Dismissed {
@@ -175,7 +155,7 @@ func (r *PRReviews) MissingReviews() []string {
return missing
}
for _, reviewer := range r.RequiredReviewers {
for _, reviewer := range r.RequestedReviewers {
if !r.IsReviewedBy(reviewer) {
missing = append(missing, reviewer)
}
@@ -188,11 +168,12 @@ func (r *PRReviews) FindReviewRequester(reviewer string) *models.TimelineComment
return nil
}
for _, t := range r.RequestedReviewers {
if t.Assignee.UserName == reviewer {
return t
for _, r := range r.FullTimeline {
if r.Type == TimelineCommentType_ReviewRequested && r.Assignee.UserName == reviewer {
return r
}
}
return nil
}
@@ -212,13 +193,6 @@ func (r *PRReviews) HasPendingReviewBy(reviewer string) bool {
}
}
// at this point, we do not have actual review by user. Check if we have a pending review
for _, t := range r.RequestedReviewers {
if t.Assignee != nil && t.Assignee.UserName == reviewer {
return true
}
}
return false
}

View File

@@ -137,61 +137,6 @@ func TestReviews(t *testing.T) {
isApproved: false,
isReviewedByTest1: true,
},
{
name: "Ghost user review",
reviews: []*models.PullReview{
{State: common.ReviewStateApproved, User: nil},
},
reviewers: []string{"user1"},
isApproved: false,
},
{
name: "ReviewRequested predates PushPull should be seen as pending",
reviews: []*models.PullReview{},
timeline: []*models.TimelineComment{
{Type: common.TimelineCommentType_PushPull},
{Type: common.TimelineCommentType_ReviewRequested, Assignee: &models.User{UserName: "user1"}},
},
reviewers: []string{"user1"},
isPendingByTest1: true,
},
{
name: "ReviewRequested postdates PushPull but blocked by older dismiss",
reviews: []*models.PullReview{},
timeline: []*models.TimelineComment{
{Type: common.TimelineCommentType_ReviewRequested, Assignee: &models.User{UserName: "user1"}},
{Type: common.TimelineCommentType_PushPull},
{Type: common.TimelineCommentType_ReviewDismissed, Assignee: &models.User{UserName: "user1"}},
},
reviewers: []string{"user1"},
isPendingByTest1: true,
},
{
name: "ReviewRequested predates PushPull should be seen as pending",
reviews: []*models.PullReview{
{ID: 101, State: common.ReviewStateRequestReview, User: &models.User{UserName: "user1"}},
},
timeline: []*models.TimelineComment{
{Type: common.TimelineCommentType_PushPull},
{Type: common.TimelineCommentType_ReviewRequested, Assignee: &models.User{UserName: "user1"}},
},
reviewers: []string{"user1"},
isPendingByTest1: true,
},
{
name: "Review requested, review, then push needs re-requesting",
reviews: []*models.PullReview{
{ID: 100, State: common.ReviewStateRequestChanges, User: &models.User{UserName: "user1"}},
},
timeline: []*models.TimelineComment{
{Type: common.TimelineCommentType_PushPull},
{Type: common.TimelineCommentType_Review, ReviewID: 100},
{Type: common.TimelineCommentType_ReviewRequested, Assignee: &models.User{UserName: "user1"}},
},
reviewers: []string{"user1"},
isReviewedByTest1: false, // Should be stale
isPendingByTest1: false, // Should be stale
},
}
for _, test := range tests {
@@ -213,7 +158,7 @@ func TestReviews(t *testing.T) {
}
return
}
reviews.SetRequiredReviewers(test.reviewers)
reviews.RequestedReviewers = test.reviewers
if r := reviews.IsApproved(); r != test.isApproved {
t.Fatal("Unexpected IsReviewed():", r, "vs. expected", test.isApproved)

View File

@@ -8,7 +8,6 @@ import (
)
const (
TimelineCommentType_ReviewDismissed = "dismiss_review"
TimelineCommentType_ReviewRequested = "review_request"
TimelineCommentType_Review = "review"
TimelineCommentType_PushPull = "pull_push"

View File

@@ -8,7 +8,6 @@ services:
gitea:
build: ./gitea
container_name: gitea-test
init: true
environment:
- GITEA_WORK_DIR=/var/lib/gitea
networks:
@@ -28,7 +27,6 @@ services:
rabbitmq:
image: rabbitmq:3.13.7-management
container_name: rabbitmq-test
init: true
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "check_running", "-q"]
interval: 30s
@@ -57,7 +55,6 @@ services:
context: ..
dockerfile: integration/gitea-events-rabbitmq-publisher/Dockerfile${GIWTF_IMAGE_SUFFIX}
container_name: gitea-publisher
init: true
networks:
- gitea-network
depends_on:
@@ -78,7 +75,6 @@ services:
context: ..
dockerfile: integration/workflow-pr/Dockerfile${GIWTF_IMAGE_SUFFIX}
container_name: workflow-pr
init: true
networks:
- gitea-network
depends_on:
@@ -107,7 +103,6 @@ services:
mock-obs:
build: ./mock-obs
container_name: mock-obs
init: true
networks:
- gitea-network
ports:
@@ -121,7 +116,6 @@ services:
context: ..
dockerfile: integration/obs-staging-bot/Dockerfile${GIWTF_IMAGE_SUFFIX}
container_name: obs-staging-bot
init: true
networks:
- gitea-network
depends_on:

View File

@@ -65,11 +65,11 @@ The testing will be conducted in a dedicated test environment that mimics the pr
| **TC-REVIEW-001** | P | **Add mandatory reviewers** | 1. Create a new PackageGit PR. | 1. All mandatory reviewers are added to both the PackageGit and ProjectGit PRs. | High |
| **TC-REVIEW-002** | - | **Add advisory reviewers** | 1. Create a new PackageGit PR with advisory reviewers defined in the configuration. | 1. Advisory reviewers are added to the PR, but their approval is not required for merging. | Medium |
| **TC-REVIEW-003** | - | **Re-add reviewers** | 1. Push a new commit to a PackageGit PR after it has been approved. | 1. The original reviewers are re-added to the PR. | Medium |
| **TC-REVIEW-004** | P | **Package PR created by a maintainer** | 1. Create a PackageGit PR from the account of a package maintainer. | 1. No review is requested from other package maintainers. | High |
| **TC-REVIEW-004** | x | **Package PR created by a maintainer** | 1. Create a PackageGit PR from the account of a package maintainer. | 1. No review is requested from other package maintainers. | High |
| **TC-REVIEW-005** | P | **Package PR created by an external user (approve)** | 1. Create a PackageGit PR from the account of a user who is not a package maintainer.<br>2. One of the package maintainers approves the PR. | 1. All package maintainers are added as reviewers.<br>2. Once one maintainer approves the PR, the other maintainers are removed as reviewers. | High |
| **TC-REVIEW-006** | P | **Package PR created by an external user (reject)** | 1. Create a PackageGit PR from the account of a user who is not a package maintainer.<br>2. One of the package maintainers rejects the PR. | 1. All package maintainers are added as reviewers.<br>2. Once one maintainer rejects the PR, the other maintainers are removed as reviewers. | High |
| **TC-REVIEW-007** | P | **Package PR created by a maintainer with ReviewRequired=true** | 1. Set `ReviewRequired = true` in `workflow.config`.<br>2. Create a PackageGit PR from the account of a package maintainer. | 1. A review is requested from other package maintainers if available. | High |
| **TC-MERGE-001** | P | **Automatic Merge** | 1. Create a PackageGit PR.<br>2. Ensure all mandatory reviews are completed on both project and package PRs. | 1. The PR is automatically merged. | High |
| **TC-MERGE-001** | x | **Automatic Merge** | 1. Create a PackageGit PR.<br>2. Ensure all mandatory reviews are completed on both project and package PRs. | 1. The PR is automatically merged. | High |
| **TC-MERGE-002** | - | **ManualMergeOnly with Package Maintainer** | 1. Create a PackageGit PR with `ManualMergeOnly` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on the package PR from the account of a package maintainer for that package. | 1. The PR is merged. | High |
| **TC-MERGE-003** | - | **ManualMergeOnly with unauthorized user** | 1. Create a PackageGit PR with `ManualMergeOnly` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on the package PR from the account of a user who is not a maintainer for that package. | 1. The PR is not merged. | High |
| **TC-MERGE-004** | - | **ManualMergeOnly with multiple packages** | 1. Create a ProjectGit PR that references multiple PackageGit PRs with `ManualMergeOnly` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on each package PR from the account of a package maintainer. | 1. The PR is merged only after "merge ok" is commented on all associated PackageGit PRs. | High |

View File

@@ -5,6 +5,7 @@ from pathlib import Path
from tests.lib.common_test_utils import GiteaAPIClient
@pytest.mark.t001
@pytest.mark.xfail(reason="The bot sometimes re-request reviews despite having all the approvals")
def test_001_automerge(automerge_env, test_user_client):
"""
Test scenario:

View File

@@ -43,6 +43,7 @@ index 0000000..e69de29
@pytest.mark.t004
@pytest.mark.xfail(reason="the bot sometimes re-requests review from autogits_obs_staging_bot despite having the approval")
def test_004_maintainer(maintainer_env, ownerA_client):
"""
Test scenario:
@@ -151,6 +152,7 @@ index 0000000..e69de29
@pytest.mark.t005
# @pytest.mark.xfail(reason="TBD troubleshoot")
def test_005_any_maintainer_approval_sufficient(maintainer_env, ownerA_client, ownerBB_client):
"""
Test scenario:

View File

@@ -27,6 +27,7 @@ import (
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"runtime/debug"
"slices"
@@ -50,10 +51,6 @@ const (
var runId uint
var GitWorkTreeAllocate func(string, string, string) (common.GitHandlerGenerator, error) = func(basePath, gitAuthor, email string) (common.GitHandlerGenerator, error) {
return common.AllocateGitWorkTree(basePath, gitAuthor, email)
}
func FetchPrGit(git common.Git, pr *models.PullRequest) error {
// clone PR head via base (target) repo
cloneURL := pr.Base.Repo.CloneURL
@@ -131,6 +128,10 @@ func ProcessBuildStatus(project *common.BuildResultList) BuildStatusSummary {
found:
for j := 0; j < len(project.Result); j++ {
common.LogDebug(" found match for @ idx:", j)
if project.Result[i].Dirty {
// ignore possible temporary failures and wait for settling
return BuildStatusSummaryBuilding
}
res := ProcessRepoBuildStatus(project.Result[i].Status)
switch res {
case BuildStatusSummarySuccess:
@@ -148,9 +149,9 @@ func ProcessBuildStatus(project *common.BuildResultList) BuildStatusSummary {
func ProcessRepoBuildStatus(results []*common.PackageBuildStatus) (status BuildStatusSummary) {
PackageBuildStatusSorter := func(a, b *common.PackageBuildStatus) int {
return strings.Compare(a.Package, b.Package)
}
PackageBuildStatusSorter := func(a, b *common.PackageBuildStatus) int {
return strings.Compare(a.Package, b.Package)
}
common.LogDebug("******* RESULTS: ")
data, _ := xml.MarshalIndent(results, "", " ")
@@ -195,23 +196,24 @@ func GetPackageBuildStatus(project *common.BuildResultList, packageName string)
return true, BuildStatusSummaryUnknown // true for 'missing'
}
// Check for any unfinished builds
// Check for any failures
for _, pkgStatus := range packageStatuses {
res, ok := common.ObsBuildStatusDetails[pkgStatus.Code]
if !ok {
common.LogInfo("unknown package result code:", pkgStatus.Code, "for package:", pkgStatus.Package)
return false, BuildStatusSummaryUnknown
}
if !res.Finished {
return false, BuildStatusSummaryBuilding
if !res.Success {
return false, BuildStatusSummaryFailed
}
}
// Check for any failures
// Check for any unfinished builds
for _, pkgStatus := range packageStatuses {
res, _ := common.ObsBuildStatusDetails[pkgStatus.Code]
if !res.Success {
return false, BuildStatusSummaryFailed
// 'ok' is already checked in the loop above
if !res.Finished {
return false, BuildStatusSummaryBuilding
}
}
@@ -219,7 +221,7 @@ func GetPackageBuildStatus(project *common.BuildResultList, packageName string)
return false, BuildStatusSummarySuccess
}
func GenerateObsPrjMeta(obs common.ObsClientInterface, 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, stagingMasterPrj string) (*common.ProjectMeta, error) {
common.LogDebug("repo content fetching ...")
err := FetchPrGit(git, pr)
if err != nil {
@@ -263,13 +265,13 @@ func GenerateObsPrjMeta(obs common.ObsClientInterface, git common.Git, gitea com
}
common.LogDebug("Trying first staging master project: ", stagingMasterPrj)
meta, err := obs.GetProjectMeta(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 = obs.GetProjectMeta(buildPrj)
meta, err = ObsClient.GetProjectMeta(buildPrj)
}
if err != nil {
common.LogError("error fetching project meta for", buildPrj, ". Err:", err)
@@ -333,10 +335,10 @@ func GenerateObsPrjMeta(obs common.ObsClientInterface, git common.Git, gitea com
// stagingProject:$buildProject
// ^- stagingProject:$buildProject:$subProjectName (based on templateProject)
func CreateQASubProject(obs common.ObsClientInterface, stagingConfig *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingProject, templateProject, subProjectName string, buildDisableRepos []string) error {
func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingProject, templateProject, subProjectName string, buildDisableRepos []string) error {
common.LogDebug("Setup QA sub projects")
common.LogDebug("reading templateProject ", templateProject)
templateMeta, err := obs.GetProjectMeta(templateProject)
templateMeta, err := ObsClient.GetProjectMeta(templateProject)
if err != nil {
common.LogError("error fetching template project meta for", templateProject, ":", err)
return err
@@ -346,10 +348,10 @@ func CreateQASubProject(obs common.ObsClientInterface, stagingConfig *common.Sta
templateMeta.Name = stagingProject + ":" + subProjectName
// freeze tag for now
if len(templateMeta.ScmSync) > 0 {
repository, err := url.Parse(templateMeta.ScmSync)
if err != nil {
panic(err)
}
repository, err := url.Parse(templateMeta.ScmSync)
if err != nil {
panic(err)
}
common.LogDebug("getting data for ", repository.EscapedPath())
split := strings.Split(repository.EscapedPath(), "/")
@@ -357,12 +359,12 @@ func CreateQASubProject(obs common.ObsClientInterface, stagingConfig *common.Sta
common.LogDebug("getting commit for ", org, " repo ", repo, " fragment ", repository.Fragment)
branch, err := gitea.GetCommit(org, repo, repository.Fragment)
if err != nil {
panic(err)
}
if err != nil {
panic(err)
}
// set expanded commit url
repository.Fragment = branch.SHA
repository.Fragment = branch.SHA
templateMeta.ScmSync = repository.String()
common.LogDebug("Setting scmsync url to ", templateMeta.ScmSync)
}
@@ -409,11 +411,11 @@ func CreateQASubProject(obs common.ObsClientInterface, stagingConfig *common.Sta
templateMeta.Repositories[idx].Paths[pidx].Project = templateMeta.Name
} else
// Check for path prefixes against a template project inside of template project area
if strings.HasPrefix(path.Project, stagingConfig.StagingProject+":") {
if strings.HasPrefix(path.Project, stagingConfig.StagingProject + ":") {
newProjectName := stagingProject
// find project name
for _, setup := range stagingConfig.QA {
if setup.Origin == path.Project {
if setup.Origin == path.Project {
common.LogDebug(" Match:", setup.Origin)
newProjectName = newProjectName + ":" + setup.Name
common.LogDebug(" New:", newProjectName)
@@ -421,14 +423,14 @@ func CreateQASubProject(obs common.ObsClientInterface, stagingConfig *common.Sta
}
}
templateMeta.Repositories[idx].Paths[pidx].Project = newProjectName
common.LogDebug(" Matched prefix")
common.LogDebug(" Matched prefix")
}
common.LogDebug(" Path using project ", templateMeta.Repositories[idx].Paths[pidx].Project)
}
}
if !IsDryRun {
err = obs.SetProjectMeta(templateMeta)
err = ObsClient.SetProjectMeta(templateMeta)
if err != nil {
common.LogError("cannot create project:", templateMeta.Name, err)
x, _ := xml.MarshalIndent(templateMeta, "", " ")
@@ -442,10 +444,10 @@ func CreateQASubProject(obs common.ObsClientInterface, stagingConfig *common.Sta
return nil
}
func StartOrUpdateBuild(obs common.ObsClientInterface, config *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest) (RequestModification, error) {
func StartOrUpdateBuild(config *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest) (RequestModification, error) {
common.LogDebug("fetching OBS project Meta")
obsPrProject := GetObsProjectAssociatedWithPr(config, obs.GetHomeProject(), pr)
meta, err := obs.GetProjectMeta(obsPrProject)
obsPrProject := GetObsProjectAssociatedWithPr(config, ObsClient.HomeProject, pr)
meta, err := ObsClient.GetProjectMeta(obsPrProject)
if err != nil {
common.LogError("error fetching project meta for", obsPrProject, ":", err)
return RequestModificationNoChange, err
@@ -470,7 +472,7 @@ func StartOrUpdateBuild(obs common.ObsClientInterface, config *common.StagingCon
if meta == nil {
// new build
common.LogDebug(" Staging master:", config.StagingProject)
meta, err = GenerateObsPrjMeta(obs, git, gitea, pr, obsPrProject, config.ObsProject, config.StagingProject)
meta, err = GenerateObsPrjMeta(git, gitea, pr, obsPrProject, config.ObsProject, config.StagingProject)
if err != nil {
return RequestModificationNoChange, err
}
@@ -482,7 +484,7 @@ func StartOrUpdateBuild(obs common.ObsClientInterface, config *common.StagingCon
common.LogDebug("Creating build project:")
common.LogDebug(" meta:", string(x))
} else {
err = obs.SetProjectMeta(meta)
err = ObsClient.SetProjectMeta(meta)
if err != nil {
x, _ := xml.MarshalIndent(meta, "", " ")
common.LogDebug(" meta:", string(x))
@@ -553,7 +555,7 @@ func ParseNotificationToPR(thread *models.NotificationThread) (org string, repo
return
}
func ProcessPullNotification(obs common.ObsClientInterface, gitea common.Gitea, thread *models.NotificationThread) {
func ProcessPullNotification(gitea common.Gitea, thread *models.NotificationThread) {
defer func() {
err := recover()
if err != nil {
@@ -569,7 +571,7 @@ func ProcessPullNotification(obs common.ObsClientInterface, gitea common.Gitea,
}
common.LogInfo("processing PR:", org, "/", repo, "#", num)
done, err := ProcessPullRequest(obs, gitea, org, repo, num)
done, err := ProcessPullRequest(gitea, org, repo, num)
if !IsDryRun && err == nil && done {
gitea.SetNotificationRead(thread.ID)
} else if err != nil {
@@ -579,7 +581,7 @@ func ProcessPullNotification(obs common.ObsClientInterface, gitea common.Gitea,
var CleanedUpIssues []int64 = []int64{}
func CleanupPullNotification(obs common.ObsClientInterface, gitea common.Gitea, thread *models.NotificationThread) (CleanupComplete bool) {
func CleanupPullNotification(gitea common.Gitea, thread *models.NotificationThread) (CleanupComplete bool) {
defer func() {
err := recover()
if err != nil {
@@ -646,8 +648,8 @@ func CleanupPullNotification(obs common.ObsClientInterface, gitea common.Gitea,
return false
}
stagingProject := GetObsProjectAssociatedWithPr(config, obs.GetHomeProject(), pr)
if prj, err := obs.GetProjectMeta(stagingProject); err != nil {
stagingProject := GetObsProjectAssociatedWithPr(config, ObsClient.HomeProject, pr)
if prj, err := ObsClient.GetProjectMeta(stagingProject); err != nil {
common.LogError("Failed fetching meta for project:", stagingProject, ". Not cleaning up")
return false
} else if prj == nil && err == nil {
@@ -661,13 +663,13 @@ func CleanupPullNotification(obs common.ObsClientInterface, gitea common.Gitea,
project := stagingProject + ":" + qa.Name
common.LogDebug("Cleaning up QA staging", project)
if !IsDryRun {
if err := obs.DeleteProject(project); err != nil {
if err := ObsClient.DeleteProject(project); err != nil {
common.LogError("Failed to cleanup QA staging", project, err)
}
}
}
if !IsDryRun {
if err := obs.DeleteProject(stagingProject); err != nil {
if err := ObsClient.DeleteProject(stagingProject); err != nil {
common.LogError("Failed to cleanup staging", stagingProject, err)
}
}
@@ -688,7 +690,7 @@ func SetStatus(gitea common.Gitea, org, repo, hash string, status *models.Commit
return err
}
func CommentPROnce(gitea common.Gitea, org string, repo string, prNum int64, msg string) {
func commentOnPackagePR(gitea common.Gitea, org string, repo string, prNum int64, msg string) {
if IsDryRun {
common.LogInfo("Would comment on package PR %s/%s#%d: %s", org, repo, prNum, msg)
return
@@ -700,18 +702,6 @@ func CommentPROnce(gitea common.Gitea, org string, repo string, prNum int64, msg
return
}
timeline, err := gitea.GetTimeline(org, repo, prNum)
if err != nil {
common.LogError("Failed to get timeline for PR %s/%s#%d: %v", org, repo, prNum, err)
return
}
for _, t := range timeline {
if t.User != nil && t.User.UserName == BotUser && t.Type == common.TimelineCommentType_Comment && t.Body == msg {
return
}
}
err = gitea.AddComment(pr, msg)
if err != nil {
common.LogError("Failed to comment on package PR %s/%s#%d: %v", org, repo, prNum, err)
@@ -719,21 +709,20 @@ func CommentPROnce(gitea common.Gitea, org string, repo string, prNum int64, msg
}
// Create and remove QA projects
func ProcessQaProjects(obs common.ObsClientInterface, stagingConfig *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingProject string) ([]string, string) {
func ProcessQaProjects(stagingConfig *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingProject string) []string {
usedQAprojects := make([]string, 0)
prLabelNames := make(map[string]int)
for _, label := range pr.Labels {
prLabelNames[label.Name] = 1
}
msg := ""
var qa_projects []string
for _, setup := range stagingConfig.QA {
QAproject := stagingProject + ":" + setup.Name
if len(setup.Label) > 0 {
if _, ok := prLabelNames[setup.Label]; !ok {
if !IsDryRun {
// blindly remove, will fail when not existing
obs.DeleteProject(QAproject)
ObsClient.DeleteProject(QAproject)
}
common.LogInfo("QA project ", setup.Name, "has no matching Label")
continue
@@ -742,25 +731,24 @@ func ProcessQaProjects(obs common.ObsClientInterface, stagingConfig *common.Stag
usedQAprojects = append(usedQAprojects, QAproject)
// check for existens first, no error, but no meta is a 404
if meta, err := obs.GetProjectMeta(QAproject); meta == nil && err == nil {
if meta, err := ObsClient.GetProjectMeta(QAproject); meta == nil && err == nil {
common.LogInfo("Create QA project ", QAproject)
CreateQASubProject(obs, stagingConfig, git, gitea, pr,
CreateQASubProject(stagingConfig, git, gitea, pr,
stagingProject,
setup.Origin,
setup.Name,
setup.BuildDisableRepos)
qa_projects = append(qa_projects, ObsWebHost+"/project/show/"+QAproject)
msg = msg + "QA Project added: " + ObsWebHost + "/project/show/" +
QAproject + "\n"
}
}
if len(qa_projects) > 0 {
msg = "Additional QA builds:\n" + strings.Join(qa_projects, "\n")
if len(msg) > 1 {
gitea.AddComment(pr, msg)
}
return usedQAprojects, msg
return usedQAprojects
}
func ProcessPullRequest(obs common.ObsClientInterface, gitea common.Gitea, org, repo string, id int64) (bool, error) {
func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, error) {
dir, err := os.MkdirTemp(os.TempDir(), BotName)
common.PanicOnError(err)
if IsDryRun {
@@ -769,7 +757,7 @@ func ProcessPullRequest(obs common.ObsClientInterface, gitea common.Gitea, org,
defer os.RemoveAll(dir)
}
gh, err := GitWorkTreeAllocate(dir, GitAuthor, "noaddress@suse.de")
gh, err := common.AllocateGitWorkTree(dir, GitAuthor, "noaddress@suse.de")
common.PanicOnError(err)
git, err := gh.CreateGitHandler(org)
@@ -814,7 +802,7 @@ func ProcessPullRequest(obs common.ObsClientInterface, gitea common.Gitea, org,
if err != nil {
common.LogError("Staging config", common.StagingConfigFile, "not found in PR to the project. Aborting.")
if !IsDryRun {
_, _ = gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Cannot find project config in PR: "+common.ProjectConfigFile)
_, err = gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Cannot find project config in PR: "+common.ProjectConfigFile)
}
return true, err
}
@@ -834,7 +822,7 @@ func ProcessPullRequest(obs common.ObsClientInterface, gitea common.Gitea, org,
return true, nil
}
meta, err := obs.GetProjectMeta(stagingConfig.ObsProject)
meta, err := ObsClient.GetProjectMeta(stagingConfig.ObsProject)
if err != nil || meta == nil {
common.LogError("Cannot find reference project meta:", stagingConfig.ObsProject, err)
if !IsDryRun && err == nil {
@@ -903,10 +891,13 @@ func ProcessPullRequest(obs common.ObsClientInterface, gitea common.Gitea, org,
if !stagingConfig.RebuildAll {
for pkg, headOid := range headSubmodules {
if baseOid, exists := baseSubmodules[pkg]; !exists || baseOid != headOid {
if pkg != "rpms" && pkg != "dependencies" {
_, spkg := filepath.Split(pkg)
if exists {
modifiedPackages = append(modifiedPackages, pkg)
modifiedPackages = append(modifiedPackages, spkg)
} else {
newPackages = append(newPackages, pkg)
newPackages = append(newPackages, spkg)
}
}
common.LogDebug(pkg, ":", baseOid, "->", headOid)
}
@@ -963,8 +954,8 @@ func ProcessPullRequest(obs common.ObsClientInterface, gitea common.Gitea, org,
}
common.LogDebug("ObsProject:", stagingConfig.ObsProject)
stagingProject := GetObsProjectAssociatedWithPr(stagingConfig, obs.GetHomeProject(), pr)
change, err := StartOrUpdateBuild(obs, stagingConfig, git, gitea, pr)
stagingProject := GetObsProjectAssociatedWithPr(stagingConfig, ObsClient.HomeProject, pr)
change, err := StartOrUpdateBuild(stagingConfig, git, gitea, pr)
status := &models.CommitStatus{
Context: BotName,
Description: "OBS Staging build",
@@ -995,8 +986,11 @@ func ProcessPullRequest(obs common.ObsClientInterface, gitea common.Gitea, org,
SetStatus(gitea, org, repo, pr.Head.Sha, status)
}
if change != RequestModificationNoChange && !IsDryRun {
gitea.AddComment(pr, msg)
}
stagingResult, err := obs.BuildStatus(stagingProject)
stagingResult, err := ObsClient.BuildStatus(stagingProject)
if err != nil {
common.LogError("failed fetching stage project status for", stagingProject, ":", err)
}
@@ -1004,14 +998,7 @@ func ProcessPullRequest(obs common.ObsClientInterface, gitea common.Gitea, org,
_, packagePRs := common.ExtractDescriptionAndPRs(bufio.NewScanner(strings.NewReader(pr.Body)))
// always update QA projects because Labels can change
qaProjects, qaProjectMsg := ProcessQaProjects(obs, stagingConfig, git, gitea, pr, stagingProject)
if change != RequestModificationNoChange && !IsDryRun {
if len(qaProjectMsg) > 0 {
msg += "\n" + qaProjectMsg
}
CommentPROnce(gitea, org, repo, id, msg)
}
qaProjects := ProcessQaProjects(stagingConfig, git, gitea, pr, stagingProject)
done := false
overallBuildStatus := ProcessBuildStatus(stagingResult)
@@ -1019,7 +1006,7 @@ func ProcessPullRequest(obs common.ObsClientInterface, gitea common.Gitea, org,
if len(qaProjects) > 0 && overallBuildStatus == BuildStatusSummarySuccess {
seperator := " in "
for _, qaProject := range qaProjects {
qaResult, err := obs.BuildStatus(qaProject)
qaResult, err := ObsClient.BuildStatus(qaProject)
if err != nil {
common.LogError("failed fetching stage project status for", qaProject, ":", err)
}
@@ -1039,6 +1026,7 @@ func ProcessPullRequest(obs common.ObsClientInterface, gitea common.Gitea, org,
}
}
}
switch overallBuildStatus {
case BuildStatusSummarySuccess:
status.Status = common.CommitStatus_Success
@@ -1079,7 +1067,7 @@ func ProcessPullRequest(obs common.ObsClientInterface, gitea common.Gitea, org,
default:
continue
}
CommentPROnce(gitea, packagePR.Org, packagePR.Repo, packagePR.Num, msg)
commentOnPackagePR(gitea, packagePR.Org, packagePR.Repo, packagePR.Num, msg)
}
if len(missingPkgs) > 0 {
@@ -1089,7 +1077,10 @@ func ProcessPullRequest(obs common.ObsClientInterface, gitea common.Gitea, org,
msg = msg + " - " + pkg + "\n"
}
common.LogInfo(msg)
CommentPROnce(gitea, org, repo, id, msg)
err := gitea.AddComment(pr, msg)
if err != nil {
common.LogError(err)
}
}
}
@@ -1108,7 +1099,8 @@ func ProcessPullRequest(obs common.ObsClientInterface, gitea common.Gitea, org,
return false, nil
}
func PollWorkNotifications(obs common.ObsClientInterface, gitea common.Gitea) {
func PollWorkNotifications(giteaUrl string) {
gitea := common.AllocateGiteaTransport(giteaUrl)
data, err := gitea.GetNotifications(common.GiteaNotificationType_Pull, nil)
if err != nil {
@@ -1124,7 +1116,7 @@ func PollWorkNotifications(obs common.ObsClientInterface, gitea common.Gitea) {
if !ListPullNotificationsOnly {
switch notification.Subject.Type {
case "Pull":
ProcessPullNotification(obs, gitea, notification)
ProcessPullNotification(gitea, notification)
default:
if !IsDryRun {
gitea.SetNotificationRead(notification.ID)
@@ -1147,7 +1139,7 @@ func PollWorkNotifications(obs common.ObsClientInterface, gitea common.Gitea) {
continue
}
cleanupFinished = CleanupPullNotification(obs, gitea, n) && cleanupFinished
cleanupFinished = CleanupPullNotification(gitea, n) && cleanupFinished
}
} else if err != nil {
common.LogError(err)
@@ -1161,8 +1153,7 @@ var ObsApiHost string
var ObsWebHost string
var IsDryRun bool
var ProcessPROnly string
var ObsClient common.ObsClientInterface
var BotUser string
var ObsClient *common.ObsClient
func ObsWebHostFromApiHost(apihost string) string {
u, err := url.Parse(apihost)
@@ -1227,18 +1218,9 @@ func main() {
}
if len(*buildRoot) > 0 {
ObsClient.SetHomeProject(*buildRoot)
ObsClient.HomeProject = *buildRoot
}
gitea := common.AllocateGiteaTransport(GiteaUrl)
user, err := gitea.GetCurrentUser()
if err != nil {
common.LogError("Cannot fetch current user:", err)
return
}
BotUser = user.UserName
if len(*ProcessPROnly) > 0 {
rx := regexp.MustCompile("^([^/#]+)/([^/#]+)#([0-9]+)$")
m := rx.FindStringSubmatch(*ProcessPROnly)
@@ -1247,14 +1229,15 @@ func main() {
return
}
gitea := common.AllocateGiteaTransport(GiteaUrl)
id, _ := strconv.ParseInt(m[3], 10, 64)
ProcessPullRequest(ObsClient, gitea, m[1], m[2], id)
ProcessPullRequest(gitea, m[1], m[2], id)
return
}
for {
PollWorkNotifications(ObsClient, gitea)
PollWorkNotifications(GiteaUrl)
common.LogInfo("Poll cycle finished")
time.Sleep(5 * time.Minute)
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,9 +6,13 @@ After=network-online.target
Type=exec
ExecStart=/usr/bin/obs-staging-bot
EnvironmentFile=-/etc/default/obs-staging-bot.env
DynamicUser=yes
NoNewPrivileges=yes
ProtectSystem=strict
User=autogits_obs_staging_bot
Group=users
# This may work when not using ssh api connections:
#DynamicUser=yes
#NoNewPrivileges=yes
#ProtectSystem=strict
[Install]
WantedBy=multi-user.target

View File

@@ -123,7 +123,7 @@ func processConfiguredRepositoryAction(action *common.RepositoryWebhookEvent, co
common.LogError(" - ", action.Repository.Name, "repo is not sha256. Ignoring.")
return
}
common.PanicOnError(git.GitExec(gitPrj, "submodule", "--quiet", "add", "--force", "--depth", "1", action.Repository.Clone_Url, action.Repository.Name))
common.PanicOnError(git.GitExec(gitPrj, "submodule", "--quiet", "add", "--force", "--depth", "1", "../" + action.Repository.Name, action.Repository.Name))
defer git.GitExecQuietOrPanic(gitPrj, "submodule", "deinit", "--all", "-f")
branch := strings.TrimSpace(git.GitExecWithOutputOrPanic(path.Join(gitPrj, action.Repository.Name), "branch", "--show-current"))
@@ -215,7 +215,7 @@ func processConfiguredPushAction(action *common.PushWebhookEvent, config *common
}
if stat, err := os.Stat(filepath.Join(git.GetPath(), gitPrj, action.Repository.Name)); err != nil {
git.GitExecOrPanic(gitPrj, "submodule", "--quiet", "add", "--force", "--depth", "1", action.Repository.Clone_Url, action.Repository.Name)
git.GitExecOrPanic(gitPrj, "submodule", "--quiet", "add", "--force", "--depth", "1", "../" + action.Repository.Name, action.Repository.Name)
common.LogDebug("Pushed to package that is not part of the project. Re-adding...", err)
} else if !stat.IsDir() {
common.LogError("Pushed to a package that is not a submodule but exists in the project. Ignoring.")
@@ -420,7 +420,7 @@ next_repo:
}
// add repository to git project
common.PanicOnError(git.GitExec(gitPrj, "submodule", "--quiet", "add", "--force", "--depth", "1", r.CloneURL, r.Name))
common.PanicOnError(git.GitExec(gitPrj, "submodule", "--quiet", "add", "--force", "--depth", "1", "../" + r.Name, r.Name))
curBranch := strings.TrimSpace(git.GitExecWithOutputOrPanic(path.Join(gitPrj, r.Name), "branch", "--show-current"))
if branch != curBranch {

View File

@@ -1 +0,0 @@
5

View File

@@ -180,6 +180,33 @@ func (pr *PRProcessor) SetSubmodulesToMatchPRSet(prset *common.PRSet) error {
}
updateSubmoduleInPR(submodulePath, prHead, git)
err := git.GitExec(path.Join(common.DefaultGitPrj, submodulePath), "lfs", "fetch")
common.LogError("lfs fetch err: ", err)
if err = git.GitExec(path.Join(common.DefaultGitPrj, submodulePath), "lfs", "fsck"); err != nil {
found_comment := false
timeline, terr := common.FetchTimelineSinceLastPush(Gitea, prHead, org, repo, idx)
if terr != nil {
common.LogError("lfs fsck error, but timeline fetch failed")
break
}
msgPrefix := "The LFS objects are broken!"
for _, t := range timeline {
if t.Type == common.TimelineCommentType_Comment && strings.HasPrefix(t.Body, msgPrefix) {
found_comment = true
common.LogError("lfs fsck Comment already found")
break
}
}
if !found_comment && !common.IsDryRun {
Gitea.AddComment(pr.PR, msgPrefix + " Please verify with 'git lfs fsck'")
}
common.LogError("lfs fsck failed with: ", err.Error())
return err
}
status, err := git.GitStatus(common.DefaultGitPrj)
common.LogDebug("status:", status)
common.LogDebug("submodule", repo, " hash:", id, " -> ", prHead)
@@ -309,14 +336,16 @@ func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
PrjGit := PrjGitPR.PR.Base.Repo
prjGitPRbranch := PrjGitPR.PR.Head.Name
if PrjGitPR.PR.Base.RepoID != PrjGitPR.PR.Head.RepoID {
PrjGitPR.RemoteName, err = git.GitClone(common.DefaultGitPrj, "", PrjGit.SSHURL)
git.GitExecOrPanic(common.DefaultGitPrj, "fetch", PrjGitPR.RemoteName, PrjGitPR.PR.Head.Sha)
git.GitExecOrPanic(common.DefaultGitPrj, "checkout", PrjGitPR.PR.Head.Sha)
common.LogInfo("Cannot update this PR as it's on another remote, not branch:", prjGitPRbranch, "Assuming this is by-design. (eg. project git PR only)")
return nil
// permission check, if submission comes from foreign repo
if !PrjGitPR.PR.AllowMaintainerEdit {
common.LogError("Warning: source and target branch are in different repositories. We may not have the right permissions...")
// Gitea.AddComment(PrjGitPR.PR, "This PR does not allow maintainer changes, but referenced package branch has changed!")
return nil
}
}
PrjGitPR.RemoteName, err = git.GitClone(common.DefaultGitPrj, prjGitPRbranch, PrjGit.SSHURL)
// PrjGitPR.RemoteName, err = git.GitClone(common.DefaultGitPrj, prjGitPRbranch, PrjGit.SSHURL)
PrjGitPR.RemoteName, err = git.GitClone(common.DefaultGitPrj, PrjGitPR.PR.Head.Ref, PrjGitPR.PR.Head.Repo.SSHURL)
common.PanicOnError(err)
git.GitExecOrPanic(common.DefaultGitPrj, "fetch", PrjGitPR.RemoteName, PrjGitBranch)
@@ -364,6 +393,7 @@ func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
}
common.PanicOnError(git.GitExec(common.DefaultGitPrj, params...))
PrjGitPR.PR.Head.Sha = newHeadCommit
Gitea.SetLabels(PrjGit.Owner.UserName, PrjGit.Name, PrjGitPR.PR.Index, []string{prset.Config.Label("PR/updated")})
}
// update PR
@@ -380,7 +410,7 @@ func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
}
return CurrentTitle == NewTitle
}
if PrjGitPR.PR.User.UserName == CurrentUser.UserName && (PrjGitPR.PR.Body != PrjGitBody || !isPrTitleSame(PrjGitPR.PR.Title, PrjGitTitle)) {
if !pr.config.NoProjectGitPR && PrjGitPR.PR.User.UserName == CurrentUser.UserName && (PrjGitPR.PR.Body != PrjGitBody || !isPrTitleSame(PrjGitPR.PR.Title, PrjGitTitle)) {
Gitea.UpdatePullRequest(PrjGit.Owner.UserName, PrjGit.Name, PrjGitPR.PR.Index, &models.EditPullRequestOption{
RemoveDeadline: true,
Title: PrjGitTitle,