20 Commits

Author SHA256 Message Date
Andrii Nikitin
833b679c96 fix reviews may be ahead of timeline
All checks were successful
go-generate-check / go-generate-check (pull_request) Successful in 27s
Integration tests / t (pull_request) Successful in 7m20s
The bot may be confused if he has all the reviews, but some of them are missing
on the timeline. This may happen if a new review arrives inbetween of gitea api call.
Changing orders of API calls ensures that state of reviews is not ahead of timeline.
2026-02-25 15:37:39 +01:00
Andrii Nikitin
6b669f1dd5 fix: correctly handle multiple reviews from the same user 2026-02-25 15:37:39 +01:00
582df2555b Merge branch 'staging-updates'
Some checks failed
go-generate-check / go-generate-check (push) Successful in 29s
Integration tests / t (push) Has been cancelled
2026-02-25 14:48:56 +01:00
91d22f7eea staging: add tests for idempotency and label changes
All checks were successful
go-generate-check / go-generate-check (pull_request) Successful in 33s
We do not want duplicate comments. And if we do have label changes,
new comments should be added.
2026-02-24 18:22:08 +01:00
913b8c8a4b staging: Match previous message format
All checks were successful
go-generate-check / go-generate-check (pull_request) Successful in 28s
Match changes in older message format. That is,

    Build is started in https://host/project/show/SUSE:SLFO:2.2:PullRequest:2162 .

    Additional QA builds:
    https://host/project/show/SUSE:SLFO:2.2:PullRequest:2162:SLES
    https://host/project/show/SUSE:SLFO:2.2:PullRequest:2162:SL-Micro

Add unit test to verify this exact format.
2026-02-24 12:23:35 +01:00
e1825dc658 staging: CommentPROnce everywhere
All checks were successful
go-generate-check / go-generate-check (pull_request) Successful in 32s
This replaces last usage of gitea.AddComment() where we do not
check if the comment already exists.
2026-02-23 19:16:40 +01:00
59965e7b5c staging: comment once on PRs using timeline
We need to comment once on PRs and verify using issue timeline
that only one comment is present

Furthermore, staging and secondary QA links should be present
in a single comment as tooling already expects this format.
2026-02-23 19:05:44 +01:00
24a4a592a7 staging: add PollWorkNotifications coverage 2026-02-23 16:01:47 +01:00
d3d9d66797 staging: add tests on commentOnPackagePR 2026-02-23 15:48:38 +01:00
7a2f7a6ee7 staging: test default projectgit repo 2026-02-23 15:44:45 +01:00
34a3a4795b staging: increase coverage of PulllRequest processing 2026-02-23 15:39:02 +01:00
bb5daebdfa staging: return correct error
Don't clobber our error before returning it
2026-02-23 15:37:41 +01:00
70bba5e239 staging: improve CreateQASubProject unit coverage 2026-02-23 15:11:30 +01:00
5793391586 staging: add core logic unit tests 2026-02-23 15:05:51 +01:00
d923db3f87 staging: tests for Notification and Review handling 2026-02-23 14:47:51 +01:00
fc4547f9a9 tests: sanitize check 2026-02-23 14:44:17 +01:00
6fa57fc4d4 staging: Fix logic error
We need to report only once all building is finished, and not partial
results. Partial results are not yet finalized, so we can only
report that build is still in progress.

Add unit tests to cover these scenarios
2026-02-23 14:33:51 +01:00
82d4e2ed5d staging: mock interface setup 2026-02-23 14:17:53 +01:00
8920644792 staging: Use interfaces allowing dependency injection
This includes also a few formatting changes
2026-02-23 14:10:10 +01:00
06772ca662 common: Add ObsClientInterface
This allows for dependency injection for future unit tests.
2026-02-23 13:43:23 +01:00
8 changed files with 1956 additions and 92 deletions

View File

@@ -83,3 +83,260 @@ 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,6 +46,15 @@ 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
@@ -57,6 +66,14 @@ 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 {

View File

@@ -17,15 +17,16 @@ type PRReviews struct {
}
func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, org, repo string, no int64) (*PRReviews, error) {
rawReviews, err := rf.GetPullRequestReviews(org, repo, no)
if err != nil {
return nil, err
}
timeline, err := rf.GetTimeline(org, repo, no)
if err != nil {
return nil, err
}
rawReviews, err := rf.GetPullRequestReviews(org, repo, no)
if err != nil {
return nil, err
}
reviews := make([]*models.PullReview, 0, 10)
needNewReviews := []string{}
@@ -187,8 +188,6 @@ func (r *PRReviews) HasPendingReviewBy(reviewer string) bool {
switch r.State {
case ReviewStateRequestReview, ReviewStatePending:
return true
default:
return false
}
}
}
@@ -201,18 +200,20 @@ func (r *PRReviews) IsReviewedBy(reviewer string) bool {
return false
}
for _, r := range r.Reviews {
if r.User.UserName == reviewer && !r.Stale {
switch r.State {
res := false
for _, i := range r.Reviews {
if i.User.UserName == reviewer && !i.Stale {
switch i.State {
case ReviewStateApproved, ReviewStateRequestChanges:
return true
default:
res = true
case ReviewStateRequestReview, ReviewStatePending:
return false
}
}
}
return false
return res
}
func (r *PRReviews) IsReviewedByOneOf(reviewers ...string) bool {

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** | 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-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-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** | 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-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-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,7 +5,6 @@ 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,7 +43,6 @@ 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:
@@ -152,7 +151,6 @@ 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

@@ -50,6 +50,10 @@ 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
@@ -144,9 +148,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, "", " ")
@@ -191,24 +195,23 @@ func GetPackageBuildStatus(project *common.BuildResultList, packageName string)
return true, BuildStatusSummaryUnknown // true for 'missing'
}
// Check for any failures
// Check for any unfinished builds
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.Success {
return false, BuildStatusSummaryFailed
if !res.Finished {
return false, BuildStatusSummaryBuilding
}
}
// Check for any unfinished builds
// Check for any failures
for _, pkgStatus := range packageStatuses {
res, _ := common.ObsBuildStatusDetails[pkgStatus.Code]
// 'ok' is already checked in the loop above
if !res.Finished {
return false, BuildStatusSummaryBuilding
if !res.Success {
return false, BuildStatusSummaryFailed
}
}
@@ -216,7 +219,7 @@ func GetPackageBuildStatus(project *common.BuildResultList, packageName string)
return false, BuildStatusSummarySuccess
}
func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingPrj, buildPrj string, stagingMasterPrj string) (*common.ProjectMeta, error) {
func GenerateObsPrjMeta(obs common.ObsClientInterface, 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 {
@@ -260,13 +263,13 @@ func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullReque
}
common.LogDebug("Trying first staging master project: ", stagingMasterPrj)
meta, err := ObsClient.GetProjectMeta(stagingMasterPrj)
meta, err := obs.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 = obs.GetProjectMeta(buildPrj)
}
if err != nil {
common.LogError("error fetching project meta for", buildPrj, ". Err:", err)
@@ -330,10 +333,10 @@ func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullReque
// stagingProject:$buildProject
// ^- stagingProject:$buildProject:$subProjectName (based on templateProject)
func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingProject, templateProject, subProjectName string, buildDisableRepos []string) error {
func CreateQASubProject(obs common.ObsClientInterface, 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 := ObsClient.GetProjectMeta(templateProject)
templateMeta, err := obs.GetProjectMeta(templateProject)
if err != nil {
common.LogError("error fetching template project meta for", templateProject, ":", err)
return err
@@ -343,10 +346,10 @@ func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, git
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(), "/")
@@ -354,12 +357,12 @@ func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, git
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)
}
@@ -406,11 +409,11 @@ func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, git
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)
@@ -418,14 +421,14 @@ func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, git
}
}
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 = ObsClient.SetProjectMeta(templateMeta)
err = obs.SetProjectMeta(templateMeta)
if err != nil {
common.LogError("cannot create project:", templateMeta.Name, err)
x, _ := xml.MarshalIndent(templateMeta, "", " ")
@@ -439,10 +442,10 @@ func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, git
return nil
}
func StartOrUpdateBuild(config *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest) (RequestModification, error) {
func StartOrUpdateBuild(obs common.ObsClientInterface, config *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest) (RequestModification, error) {
common.LogDebug("fetching OBS project Meta")
obsPrProject := GetObsProjectAssociatedWithPr(config, ObsClient.HomeProject, pr)
meta, err := ObsClient.GetProjectMeta(obsPrProject)
obsPrProject := GetObsProjectAssociatedWithPr(config, obs.GetHomeProject(), pr)
meta, err := obs.GetProjectMeta(obsPrProject)
if err != nil {
common.LogError("error fetching project meta for", obsPrProject, ":", err)
return RequestModificationNoChange, err
@@ -467,7 +470,7 @@ func StartOrUpdateBuild(config *common.StagingConfig, git common.Git, gitea comm
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(obs, git, gitea, pr, obsPrProject, config.ObsProject, config.StagingProject)
if err != nil {
return RequestModificationNoChange, err
}
@@ -479,7 +482,7 @@ func StartOrUpdateBuild(config *common.StagingConfig, git common.Git, gitea comm
common.LogDebug("Creating build project:")
common.LogDebug(" meta:", string(x))
} else {
err = ObsClient.SetProjectMeta(meta)
err = obs.SetProjectMeta(meta)
if err != nil {
x, _ := xml.MarshalIndent(meta, "", " ")
common.LogDebug(" meta:", string(x))
@@ -550,7 +553,7 @@ func ParseNotificationToPR(thread *models.NotificationThread) (org string, repo
return
}
func ProcessPullNotification(gitea common.Gitea, thread *models.NotificationThread) {
func ProcessPullNotification(obs common.ObsClientInterface, gitea common.Gitea, thread *models.NotificationThread) {
defer func() {
err := recover()
if err != nil {
@@ -566,7 +569,7 @@ func ProcessPullNotification(gitea common.Gitea, thread *models.NotificationThre
}
common.LogInfo("processing PR:", org, "/", repo, "#", num)
done, err := ProcessPullRequest(gitea, org, repo, num)
done, err := ProcessPullRequest(obs, gitea, org, repo, num)
if !IsDryRun && err == nil && done {
gitea.SetNotificationRead(thread.ID)
} else if err != nil {
@@ -576,7 +579,7 @@ func ProcessPullNotification(gitea common.Gitea, thread *models.NotificationThre
var CleanedUpIssues []int64 = []int64{}
func CleanupPullNotification(gitea common.Gitea, thread *models.NotificationThread) (CleanupComplete bool) {
func CleanupPullNotification(obs common.ObsClientInterface, gitea common.Gitea, thread *models.NotificationThread) (CleanupComplete bool) {
defer func() {
err := recover()
if err != nil {
@@ -643,8 +646,8 @@ func CleanupPullNotification(gitea common.Gitea, thread *models.NotificationThre
return false
}
stagingProject := GetObsProjectAssociatedWithPr(config, ObsClient.HomeProject, pr)
if prj, err := ObsClient.GetProjectMeta(stagingProject); err != nil {
stagingProject := GetObsProjectAssociatedWithPr(config, obs.GetHomeProject(), pr)
if prj, err := obs.GetProjectMeta(stagingProject); err != nil {
common.LogError("Failed fetching meta for project:", stagingProject, ". Not cleaning up")
return false
} else if prj == nil && err == nil {
@@ -658,13 +661,13 @@ func CleanupPullNotification(gitea common.Gitea, thread *models.NotificationThre
project := stagingProject + ":" + qa.Name
common.LogDebug("Cleaning up QA staging", project)
if !IsDryRun {
if err := ObsClient.DeleteProject(project); err != nil {
if err := obs.DeleteProject(project); err != nil {
common.LogError("Failed to cleanup QA staging", project, err)
}
}
}
if !IsDryRun {
if err := ObsClient.DeleteProject(stagingProject); err != nil {
if err := obs.DeleteProject(stagingProject); err != nil {
common.LogError("Failed to cleanup staging", stagingProject, err)
}
}
@@ -685,7 +688,7 @@ func SetStatus(gitea common.Gitea, org, repo, hash string, status *models.Commit
return err
}
func commentOnPackagePR(gitea common.Gitea, org string, repo string, prNum int64, msg string) {
func CommentPROnce(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
@@ -697,6 +700,18 @@ func commentOnPackagePR(gitea common.Gitea, org string, repo string, prNum int64
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)
@@ -704,20 +719,21 @@ func commentOnPackagePR(gitea common.Gitea, org string, repo string, prNum int64
}
// Create and remove QA projects
func ProcessQaProjects(stagingConfig *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingProject string) []string {
func ProcessQaProjects(obs common.ObsClientInterface, stagingConfig *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingProject string) ([]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
ObsClient.DeleteProject(QAproject)
obs.DeleteProject(QAproject)
}
common.LogInfo("QA project ", setup.Name, "has no matching Label")
continue
@@ -726,24 +742,25 @@ func ProcessQaProjects(stagingConfig *common.StagingConfig, git common.Git, gite
usedQAprojects = append(usedQAprojects, QAproject)
// check for existens first, no error, but no meta is a 404
if meta, err := ObsClient.GetProjectMeta(QAproject); meta == nil && err == nil {
if meta, err := obs.GetProjectMeta(QAproject); meta == nil && err == nil {
common.LogInfo("Create QA project ", QAproject)
CreateQASubProject(stagingConfig, git, gitea, pr,
CreateQASubProject(obs, stagingConfig, git, gitea, pr,
stagingProject,
setup.Origin,
setup.Name,
setup.BuildDisableRepos)
msg = msg + "QA Project added: " + ObsWebHost + "/project/show/" +
QAproject + "\n"
qa_projects = append(qa_projects, ObsWebHost+"/project/show/"+QAproject)
}
}
if len(msg) > 1 {
gitea.AddComment(pr, msg)
if len(qa_projects) > 0 {
msg = "Additional QA builds:\n" + strings.Join(qa_projects, "\n")
}
return usedQAprojects
return usedQAprojects, msg
}
func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, error) {
func ProcessPullRequest(obs common.ObsClientInterface, gitea common.Gitea, org, repo string, id int64) (bool, error) {
dir, err := os.MkdirTemp(os.TempDir(), BotName)
common.PanicOnError(err)
if IsDryRun {
@@ -752,7 +769,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
defer os.RemoveAll(dir)
}
gh, err := common.AllocateGitWorkTree(dir, GitAuthor, "noaddress@suse.de")
gh, err := GitWorkTreeAllocate(dir, GitAuthor, "noaddress@suse.de")
common.PanicOnError(err)
git, err := gh.CreateGitHandler(org)
@@ -797,7 +814,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
if err != nil {
common.LogError("Staging config", common.StagingConfigFile, "not found in PR to the project. Aborting.")
if !IsDryRun {
_, err = gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Cannot find project config in PR: "+common.ProjectConfigFile)
_, _ = gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Cannot find project config in PR: "+common.ProjectConfigFile)
}
return true, err
}
@@ -817,7 +834,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
return true, nil
}
meta, err := ObsClient.GetProjectMeta(stagingConfig.ObsProject)
meta, err := obs.GetProjectMeta(stagingConfig.ObsProject)
if err != nil || meta == nil {
common.LogError("Cannot find reference project meta:", stagingConfig.ObsProject, err)
if !IsDryRun && err == nil {
@@ -946,8 +963,8 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
}
common.LogDebug("ObsProject:", stagingConfig.ObsProject)
stagingProject := GetObsProjectAssociatedWithPr(stagingConfig, ObsClient.HomeProject, pr)
change, err := StartOrUpdateBuild(stagingConfig, git, gitea, pr)
stagingProject := GetObsProjectAssociatedWithPr(stagingConfig, obs.GetHomeProject(), pr)
change, err := StartOrUpdateBuild(obs, stagingConfig, git, gitea, pr)
status := &models.CommitStatus{
Context: BotName,
Description: "OBS Staging build",
@@ -978,11 +995,8 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
SetStatus(gitea, org, repo, pr.Head.Sha, status)
}
if change != RequestModificationNoChange && !IsDryRun {
gitea.AddComment(pr, msg)
}
stagingResult, err := ObsClient.BuildStatus(stagingProject)
stagingResult, err := obs.BuildStatus(stagingProject)
if err != nil {
common.LogError("failed fetching stage project status for", stagingProject, ":", err)
}
@@ -990,7 +1004,14 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
_, packagePRs := common.ExtractDescriptionAndPRs(bufio.NewScanner(strings.NewReader(pr.Body)))
// always update QA projects because Labels can change
qaProjects := ProcessQaProjects(stagingConfig, git, gitea, pr, stagingProject)
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)
}
done := false
overallBuildStatus := ProcessBuildStatus(stagingResult)
@@ -998,7 +1019,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
if len(qaProjects) > 0 && overallBuildStatus == BuildStatusSummarySuccess {
seperator := " in "
for _, qaProject := range qaProjects {
qaResult, err := ObsClient.BuildStatus(qaProject)
qaResult, err := obs.BuildStatus(qaProject)
if err != nil {
common.LogError("failed fetching stage project status for", qaProject, ":", err)
}
@@ -1058,7 +1079,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
default:
continue
}
commentOnPackagePR(gitea, packagePR.Org, packagePR.Repo, packagePR.Num, msg)
CommentPROnce(gitea, packagePR.Org, packagePR.Repo, packagePR.Num, msg)
}
if len(missingPkgs) > 0 {
@@ -1068,10 +1089,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
msg = msg + " - " + pkg + "\n"
}
common.LogInfo(msg)
err := gitea.AddComment(pr, msg)
if err != nil {
common.LogError(err)
}
CommentPROnce(gitea, org, repo, id, msg)
}
}
@@ -1090,8 +1108,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
return false, nil
}
func PollWorkNotifications(giteaUrl string) {
gitea := common.AllocateGiteaTransport(giteaUrl)
func PollWorkNotifications(obs common.ObsClientInterface, gitea common.Gitea) {
data, err := gitea.GetNotifications(common.GiteaNotificationType_Pull, nil)
if err != nil {
@@ -1107,7 +1124,7 @@ func PollWorkNotifications(giteaUrl string) {
if !ListPullNotificationsOnly {
switch notification.Subject.Type {
case "Pull":
ProcessPullNotification(gitea, notification)
ProcessPullNotification(obs, gitea, notification)
default:
if !IsDryRun {
gitea.SetNotificationRead(notification.ID)
@@ -1130,7 +1147,7 @@ func PollWorkNotifications(giteaUrl string) {
continue
}
cleanupFinished = CleanupPullNotification(gitea, n) && cleanupFinished
cleanupFinished = CleanupPullNotification(obs, gitea, n) && cleanupFinished
}
} else if err != nil {
common.LogError(err)
@@ -1144,7 +1161,8 @@ var ObsApiHost string
var ObsWebHost string
var IsDryRun bool
var ProcessPROnly string
var ObsClient *common.ObsClient
var ObsClient common.ObsClientInterface
var BotUser string
func ObsWebHostFromApiHost(apihost string) string {
u, err := url.Parse(apihost)
@@ -1209,9 +1227,18 @@ func main() {
}
if len(*buildRoot) > 0 {
ObsClient.HomeProject = *buildRoot
ObsClient.SetHomeProject(*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)
@@ -1220,15 +1247,14 @@ func main() {
return
}
gitea := common.AllocateGiteaTransport(GiteaUrl)
id, _ := strconv.ParseInt(m[3], 10, 64)
ProcessPullRequest(gitea, m[1], m[2], id)
ProcessPullRequest(ObsClient, gitea, m[1], m[2], id)
return
}
for {
PollWorkNotifications(GiteaUrl)
PollWorkNotifications(ObsClient, gitea)
common.LogInfo("Poll cycle finished")
time.Sleep(5 * time.Minute)
}

File diff suppressed because it is too large Load Diff