Compare commits
1 Commits
t-mergemod
...
status
| Author | SHA256 | Date | |
|---|---|---|---|
| a7784977f9 |
@@ -39,10 +39,6 @@ const (
|
||||
|
||||
Permission_ForceMerge = "force-merge"
|
||||
Permission_Group = "release-engineering"
|
||||
|
||||
MergeModeFF = "ff-only"
|
||||
MergeModeReplace = "replace"
|
||||
MergeModeDevel = "devel"
|
||||
)
|
||||
|
||||
type ConfigFile struct {
|
||||
@@ -56,9 +52,9 @@ type ReviewGroup struct {
|
||||
}
|
||||
|
||||
type QAConfig struct {
|
||||
Name string
|
||||
Origin string
|
||||
Label string // requires this gitea lable to be set or skipped
|
||||
Name string
|
||||
Origin string
|
||||
Label string // requires this gitea lable to be set or skipped
|
||||
BuildDisableRepos []string // which repos to build disable in the new project
|
||||
}
|
||||
|
||||
@@ -93,8 +89,7 @@ type AutogitConfig struct {
|
||||
Committers []string // group in addition to Reviewers and Maintainers that can order the bot around, mostly as helper for factory-maintainers
|
||||
Subdirs []string // list of directories to sort submodules into. Needed b/c _manifest cannot list non-existent directories
|
||||
|
||||
Labels map[string]string // list of tags, if not default, to apply
|
||||
MergeMode string // project merge mode
|
||||
Labels map[string]string // list of tags, if not default, to apply
|
||||
|
||||
NoProjectGitPR bool // do not automatically create project git PRs, just assign reviewers and assume somethign else creates the ProjectGit PR
|
||||
ManualMergeOnly bool // only merge with "Merge OK" comment by Project Maintainers and/or Package Maintainers and/or reviewers
|
||||
@@ -189,17 +184,6 @@ func ReadWorkflowConfig(gitea GiteaFileContentAndRepoFetcher, git_project string
|
||||
}
|
||||
}
|
||||
config.GitProjectName = config.GitProjectName + "#" + branch
|
||||
|
||||
// verify merge modes
|
||||
switch config.MergeMode {
|
||||
case MergeModeFF, MergeModeDevel, MergeModeReplace:
|
||||
break // good results
|
||||
case "":
|
||||
config.MergeMode = MergeModeFF
|
||||
default:
|
||||
return nil, fmt.Errorf("Unsupported merge mode in %s: %s", git_project, config.MergeMode)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -341,67 +341,3 @@ func TestConfigPermissions(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigMergeModeParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
json string
|
||||
mergeMode string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
json: "{}",
|
||||
mergeMode: common.MergeModeFF,
|
||||
},
|
||||
{
|
||||
name: "ff-only",
|
||||
json: `{"MergeMode": "ff-only"}`,
|
||||
mergeMode: common.MergeModeFF,
|
||||
},
|
||||
{
|
||||
name: "replace",
|
||||
json: `{"MergeMode": "replace"}`,
|
||||
mergeMode: common.MergeModeReplace,
|
||||
},
|
||||
{
|
||||
name: "devel",
|
||||
json: `{"MergeMode": "devel"}`,
|
||||
mergeMode: common.MergeModeDevel,
|
||||
},
|
||||
{
|
||||
name: "unsupported",
|
||||
json: `{"MergeMode": "invalid"}`,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
repo := models.Repository{
|
||||
DefaultBranch: "master",
|
||||
}
|
||||
|
||||
ctl := gomock.NewController(t)
|
||||
gitea := mock_common.NewMockGiteaFileContentAndRepoFetcher(ctl)
|
||||
gitea.EXPECT().GetRepositoryFileContent("foo", "bar", "", "workflow.config").Return([]byte(test.json), "abc", nil)
|
||||
gitea.EXPECT().GetRepository("foo", "bar").Return(&repo, nil)
|
||||
|
||||
config, err := common.ReadWorkflowConfig(gitea, "foo/bar")
|
||||
if test.wantErr {
|
||||
if err == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if config.MergeMode != test.mergeMode {
|
||||
t.Errorf("Expected MergeMode %s, got %s", test.mergeMode, config.MergeMode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
155
common/pr.go
155
common/pr.go
@@ -554,144 +554,6 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
|
||||
return is_manually_reviewed_ok
|
||||
}
|
||||
|
||||
func (rs *PRSet) AddMergeCommit(git Git, remote string, pr int) bool {
|
||||
prinfo := rs.PRs[pr]
|
||||
|
||||
LogDebug("Adding merge commit for %s", PRtoString(prinfo.PR))
|
||||
if !prinfo.PR.AllowMaintainerEdit {
|
||||
LogError(" PR is not editable by maintainer")
|
||||
return false
|
||||
}
|
||||
|
||||
repo := prinfo.PR.Base.Repo
|
||||
head := prinfo.PR.Head
|
||||
br := rs.Config.Branch
|
||||
if len(br) == 0 {
|
||||
br = prinfo.PR.Base.Name
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("Merge branch '%s' into %s", br, head.Name)
|
||||
if err := git.GitExec(repo.Name, "merge", "--no-ff", "--no-commit", "-X", "theirs", head.Sha); err != nil {
|
||||
if err := git.GitExec(repo.Name, "merge", "--no-ff", "--no-commit", "--allow-unrelated-histories", "-X", "theirs", head.Sha); err != nil {
|
||||
return false
|
||||
}
|
||||
LogError("WARNING: Merging unrelated histories")
|
||||
}
|
||||
|
||||
// ensure only files that are in head.Sha are kept
|
||||
git.GitExecOrPanic(repo.Name, "read-tree", "--reset", "-u", head.Sha)
|
||||
git.GitExecOrPanic(repo.Name, "commit", "-m", msg)
|
||||
|
||||
if !IsDryRun {
|
||||
git.GitExecOrPanic(repo.Name, "push", remote, "HEAD:"+head.Name)
|
||||
prinfo.PR.Head.Sha = strings.TrimSpace(git.GitExecWithOutputOrPanic(repo.Name, "rev-list", "-1", "HEAD")) // need to update as it's pushed but pr not refetched
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (rs *PRSet) HasMerge(git Git, pr int) bool {
|
||||
prinfo := rs.PRs[pr]
|
||||
|
||||
repo := prinfo.PR.Base.Repo
|
||||
head := prinfo.PR.Head
|
||||
br := rs.Config.Branch
|
||||
if len(br) == 0 {
|
||||
br = prinfo.PR.Base.Name
|
||||
}
|
||||
|
||||
parents, err := git.GitExecWithOutput(repo.Name, "show", "-s", "--format=%P", head.Sha)
|
||||
if err == nil {
|
||||
p := strings.Fields(strings.TrimSpace(parents))
|
||||
if len(p) == 2 {
|
||||
targetHead, _ := git.GitExecWithOutput(repo.Name, "rev-parse", "HEAD")
|
||||
targetHead = strings.TrimSpace(targetHead)
|
||||
if p[0] == targetHead || p[1] == targetHead {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (rs *PRSet) PrepareForMerge(git Git) bool {
|
||||
// verify that package can merge here. Checkout current target branch of each PRSet, make a temporary branch
|
||||
// PR_#_mergetest and perform the merge based
|
||||
|
||||
if rs.Config.MergeMode == MergeModeDevel {
|
||||
return true // always can merge as we set branch here, not merge anything
|
||||
} else {
|
||||
// make sure that all the package PRs are in mergeable state
|
||||
for idx, prinfo := range rs.PRs {
|
||||
if rs.IsPrjGitPR(prinfo.PR) {
|
||||
continue
|
||||
}
|
||||
|
||||
repo := prinfo.PR.Base.Repo
|
||||
head := prinfo.PR.Head
|
||||
br := rs.Config.Branch
|
||||
if len(br) == 0 {
|
||||
br = prinfo.PR.Base.Name
|
||||
}
|
||||
|
||||
remote, err := git.GitClone(repo.Name, br, repo.SSHURL)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
git.GitExecOrPanic(repo.Name, "fetch", remote, head.Sha)
|
||||
switch rs.Config.MergeMode {
|
||||
case MergeModeFF:
|
||||
if err := git.GitExec(repo.Name, "merge-base", "--is-ancestor", "HEAD", head.Sha); err != nil {
|
||||
return false
|
||||
}
|
||||
case MergeModeReplace:
|
||||
Verify:
|
||||
if err := git.GitExec(repo.Name, "merge-base", "--is-ancestor", "HEAD", head.Sha); err != nil {
|
||||
if !rs.HasMerge(git, idx) {
|
||||
forkRemote, err := git.GitClone(repo.Name, head.Name, head.Repo.SSHURL)
|
||||
if err != nil {
|
||||
LogError("Failed to clone head repo:", head.Name, head.Repo.SSHURL)
|
||||
return false
|
||||
}
|
||||
LogDebug("Merge commit is missing and this is not FF merge possibility")
|
||||
git.GitExecOrPanic(repo.Name, "checkout", remote+"/"+br)
|
||||
if !rs.AddMergeCommit(git, forkRemote, idx) {
|
||||
return false
|
||||
}
|
||||
if !IsDryRun {
|
||||
goto Verify
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now we check project git if mergeable
|
||||
prjgit_info, err := rs.GetPrjGitPR()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
prjgit := prjgit_info.PR
|
||||
|
||||
_, _, prjgitBranch := rs.Config.GetPrjGit()
|
||||
remote, err := git.GitClone(DefaultGitPrj, prjgitBranch, prjgit.Base.Repo.SSHURL)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
testBranch := fmt.Sprintf("PR_%d_mergetest", prjgit.Index)
|
||||
git.GitExecOrPanic(DefaultGitPrj, "fetch", remote, prjgit.Head.Sha)
|
||||
if err := git.GitExec(DefaultGitPrj, "checkout", "-B", testBranch, prjgit.Base.Sha); err != nil {
|
||||
return false
|
||||
}
|
||||
if err := git.GitExec(DefaultGitPrj, "merge", "--no-ff", "--no-commit", prjgit.Head.Sha); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
|
||||
prjgit_info, err := rs.GetPrjGitPR()
|
||||
if err != nil {
|
||||
@@ -856,10 +718,11 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
|
||||
prinfo.RemoteName, err = git.GitClone(repo.Name, br, repo.SSHURL)
|
||||
PanicOnError(err)
|
||||
git.GitExecOrPanic(repo.Name, "fetch", prinfo.RemoteName, head.Sha)
|
||||
if rs.Config.MergeMode == MergeModeDevel || isNewRepo {
|
||||
git.GitExecOrPanic(repo.Name, "checkout", "-B", br, head.Sha)
|
||||
|
||||
if isNewRepo {
|
||||
LogInfo("Force-pushing new repository branch", br, "to", head.Sha)
|
||||
// we don't merge, we just set the branch to this commit
|
||||
} else {
|
||||
git.GitExecOrPanic(repo.Name, "fetch", prinfo.RemoteName, head.Sha)
|
||||
git.GitExecOrPanic(repo.Name, "merge", "--ff", head.Sha)
|
||||
}
|
||||
}
|
||||
@@ -885,15 +748,11 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
|
||||
}
|
||||
|
||||
if !IsDryRun {
|
||||
params := []string{"push"}
|
||||
if rs.Config.MergeMode == MergeModeDevel || isNewRepo {
|
||||
params = append(params, "-f")
|
||||
}
|
||||
params = append(params, prinfo.RemoteName)
|
||||
if isNewRepo {
|
||||
params = append(params, prinfo.PR.Head.Sha+":"+prinfo.PR.Base.Name)
|
||||
git.GitExecOrPanic(repo.Name, "push", "-f", prinfo.RemoteName, prinfo.PR.Head.Sha+":"+prinfo.PR.Base.Name)
|
||||
} else {
|
||||
git.GitExecOrPanic(repo.Name, "push", prinfo.RemoteName)
|
||||
}
|
||||
git.GitExecOrPanic(repo.Name, params...)
|
||||
} else {
|
||||
LogInfo("*** WOULD push", repo.Name, "to", prinfo.RemoteName)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package common_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@@ -1268,7 +1267,7 @@ func TestPRMerge(t *testing.T) {
|
||||
Owner: &models.User{
|
||||
UserName: "org",
|
||||
},
|
||||
SSHURL: "ssh://git@src.opensuse.org/org/prj.git",
|
||||
SSHURL: "file://" + path.Join(repoDir, "prjgit"),
|
||||
},
|
||||
},
|
||||
Head: &models.PRBranchInfo{
|
||||
@@ -1290,7 +1289,7 @@ func TestPRMerge(t *testing.T) {
|
||||
Owner: &models.User{
|
||||
UserName: "org",
|
||||
},
|
||||
SSHURL: "ssh://git@src.opensuse.org/org/prj.git",
|
||||
SSHURL: "file://" + path.Join(cmd.Dir, "prjgit"),
|
||||
},
|
||||
},
|
||||
Head: &models.PRBranchInfo{
|
||||
@@ -1400,344 +1399,3 @@ func TestPRChanges(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRPrepareForMerge(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(*mock_common.MockGit, *models.PullRequest, *models.PullRequest)
|
||||
config *common.AutogitConfig
|
||||
expected bool
|
||||
editable bool
|
||||
}{
|
||||
{
|
||||
name: "Success Devel",
|
||||
config: &common.AutogitConfig{
|
||||
Organization: "org",
|
||||
GitProjectName: "org/_ObsPrj#master",
|
||||
MergeMode: common.MergeModeDevel,
|
||||
},
|
||||
setup: func(m *mock_common.MockGit, prjPR, pkgPR *models.PullRequest) {},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Success FF",
|
||||
config: &common.AutogitConfig{
|
||||
Organization: "org",
|
||||
GitProjectName: "org/_ObsPrj#master",
|
||||
MergeMode: common.MergeModeFF,
|
||||
},
|
||||
setup: func(m *mock_common.MockGit, prjPR, pkgPR *models.PullRequest) {
|
||||
m.EXPECT().GitClone("pkg", "master", pkgPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "fetch", "origin", pkgPR.Head.Sha)
|
||||
m.EXPECT().GitExec("pkg", "merge-base", "--is-ancestor", "HEAD", pkgPR.Head.Sha).Return(nil)
|
||||
|
||||
m.EXPECT().GitClone("_ObsPrj", "master", prjPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("_ObsPrj", "fetch", "origin", prjPR.Head.Sha)
|
||||
m.EXPECT().GitExec("_ObsPrj", "checkout", "-B", "PR_1_mergetest", prjPR.Base.Sha).Return(nil)
|
||||
m.EXPECT().GitExec("_ObsPrj", "merge", "--no-ff", "--no-commit", prjPR.Head.Sha).Return(nil)
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Success Replace MergeCommit",
|
||||
config: &common.AutogitConfig{
|
||||
Organization: "org",
|
||||
GitProjectName: "org/_ObsPrj#master",
|
||||
MergeMode: common.MergeModeReplace,
|
||||
},
|
||||
setup: func(m *mock_common.MockGit, prjPR, pkgPR *models.PullRequest) {
|
||||
m.EXPECT().GitClone("pkg", "master", pkgPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "fetch", "origin", pkgPR.Head.Sha)
|
||||
// merge-base fails initially
|
||||
m.EXPECT().GitExec("pkg", "merge-base", "--is-ancestor", "HEAD", pkgPR.Head.Sha).Return(fmt.Errorf("not ancestor"))
|
||||
// HasMerge returns true
|
||||
m.EXPECT().GitExecWithOutput("pkg", "show", "-s", "--format=%P", pkgPR.Head.Sha).Return("parent1 target_head", nil)
|
||||
m.EXPECT().GitExecWithOutput("pkg", "rev-parse", "HEAD").Return("target_head", nil)
|
||||
|
||||
m.EXPECT().GitClone("_ObsPrj", "master", prjPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("_ObsPrj", "fetch", "origin", prjPR.Head.Sha)
|
||||
m.EXPECT().GitExec("_ObsPrj", "checkout", "-B", "PR_1_mergetest", prjPR.Base.Sha).Return(nil)
|
||||
m.EXPECT().GitExec("_ObsPrj", "merge", "--no-ff", "--no-commit", prjPR.Head.Sha).Return(nil)
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Merge Conflict in PrjGit",
|
||||
config: &common.AutogitConfig{
|
||||
Organization: "org",
|
||||
GitProjectName: "org/_ObsPrj#master",
|
||||
MergeMode: common.MergeModeFF,
|
||||
},
|
||||
setup: func(m *mock_common.MockGit, prjPR, pkgPR *models.PullRequest) {
|
||||
m.EXPECT().GitClone("pkg", "master", pkgPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "fetch", "origin", pkgPR.Head.Sha)
|
||||
m.EXPECT().GitExec("pkg", "merge-base", "--is-ancestor", "HEAD", pkgPR.Head.Sha).Return(nil)
|
||||
|
||||
m.EXPECT().GitClone("_ObsPrj", "master", prjPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("_ObsPrj", "fetch", "origin", prjPR.Head.Sha)
|
||||
m.EXPECT().GitExec("_ObsPrj", "checkout", "-B", "PR_1_mergetest", prjPR.Base.Sha).Return(nil)
|
||||
m.EXPECT().GitExec("_ObsPrj", "merge", "--no-ff", "--no-commit", prjPR.Head.Sha).Return(fmt.Errorf("conflict"))
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Not FF in PkgGit",
|
||||
config: &common.AutogitConfig{
|
||||
Organization: "org",
|
||||
GitProjectName: "org/_ObsPrj#master",
|
||||
MergeMode: common.MergeModeFF,
|
||||
},
|
||||
setup: func(m *mock_common.MockGit, prjPR, pkgPR *models.PullRequest) {
|
||||
m.EXPECT().GitClone("pkg", "master", pkgPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "fetch", "origin", pkgPR.Head.Sha)
|
||||
m.EXPECT().GitExec("pkg", "merge-base", "--is-ancestor", "HEAD", pkgPR.Head.Sha).Return(fmt.Errorf("not ancestor"))
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Success Replace with AddMergeCommit",
|
||||
config: &common.AutogitConfig{
|
||||
Organization: "org",
|
||||
GitProjectName: "org/_ObsPrj#master",
|
||||
MergeMode: common.MergeModeReplace,
|
||||
},
|
||||
editable: true,
|
||||
setup: func(m *mock_common.MockGit, prjPR, pkgPR *models.PullRequest) {
|
||||
m.EXPECT().GitClone("pkg", "master", pkgPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "fetch", "origin", pkgPR.Head.Sha)
|
||||
// First merge-base fails
|
||||
m.EXPECT().GitExec("pkg", "merge-base", "--is-ancestor", "HEAD", pkgPR.Head.Sha).Return(fmt.Errorf("not ancestor"))
|
||||
// HasMerge returns false
|
||||
m.EXPECT().GitExecWithOutput("pkg", "show", "-s", "--format=%P", pkgPR.Head.Sha).Return("parent1", nil)
|
||||
m.EXPECT().GitClone("pkg", pkgPR.Head.Name, pkgPR.Base.Repo.SSHURL).Return("origin_fork", nil)
|
||||
// AddMergeCommit is called
|
||||
m.EXPECT().GitExecOrPanic("pkg", "checkout", "origin/master")
|
||||
m.EXPECT().GitExec("pkg", "merge", "--no-ff", "--no-commit", "-X", "theirs", pkgPR.Head.Sha).Return(nil)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "read-tree", "--reset", "-u", pkgPR.Head.Sha)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "commit", "-m", gomock.Any())
|
||||
m.EXPECT().GitExecOrPanic("pkg", "push", "origin_fork", "HEAD:"+pkgPR.Head.Name)
|
||||
m.EXPECT().GitExecWithOutputOrPanic("pkg", "rev-list", "-1", "HEAD").Return("new_pkg_head_sha")
|
||||
// Second merge-base succeeds (after goto Verify)
|
||||
m.EXPECT().GitExec("pkg", "merge-base", "--is-ancestor", "HEAD", "new_pkg_head_sha").Return(nil)
|
||||
|
||||
m.EXPECT().GitClone("_ObsPrj", "master", prjPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("_ObsPrj", "fetch", "origin", prjPR.Head.Sha)
|
||||
m.EXPECT().GitExec("_ObsPrj", "checkout", "-B", "PR_1_mergetest", prjPR.Base.Sha).Return(nil)
|
||||
m.EXPECT().GitExec("_ObsPrj", "merge", "--no-ff", "--no-commit", prjPR.Head.Sha).Return(nil)
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
prjPR := &models.PullRequest{
|
||||
Index: 1,
|
||||
Base: &models.PRBranchInfo{
|
||||
Name: "master",
|
||||
Sha: "base_sha",
|
||||
Repo: &models.Repository{
|
||||
Owner: &models.User{UserName: "org"},
|
||||
Name: "_ObsPrj",
|
||||
SSHURL: "ssh://git@src.opensuse.org/org/_ObsPrj.git",
|
||||
},
|
||||
},
|
||||
Head: &models.PRBranchInfo{
|
||||
Sha: "head_sha",
|
||||
Repo: &models.Repository{
|
||||
Owner: &models.User{UserName: "org"},
|
||||
Name: "_ObsPrj",
|
||||
SSHURL: "ssh://git@src.opensuse.org/org/_ObsPrj.git",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pkgPR := &models.PullRequest{
|
||||
Index: 2,
|
||||
Base: &models.PRBranchInfo{
|
||||
Name: "master",
|
||||
Sha: "pkg_base_sha",
|
||||
Repo: &models.Repository{
|
||||
Owner: &models.User{UserName: "org"},
|
||||
Name: "pkg",
|
||||
SSHURL: "ssh://git@src.opensuse.org/org/pkg.git",
|
||||
},
|
||||
},
|
||||
Head: &models.PRBranchInfo{
|
||||
Name: "branch_name",
|
||||
Sha: "pkg_head_sha",
|
||||
Repo: &models.Repository{
|
||||
Owner: &models.User{UserName: "org"},
|
||||
Name: "pkg",
|
||||
SSHURL: "ssh://git@src.opensuse.org/org/pkg.git",
|
||||
},
|
||||
},
|
||||
AllowMaintainerEdit: test.editable,
|
||||
}
|
||||
|
||||
ctl := gomock.NewController(t)
|
||||
git := mock_common.NewMockGit(ctl)
|
||||
test.setup(git, prjPR, pkgPR)
|
||||
|
||||
prset := &common.PRSet{
|
||||
Config: test.config,
|
||||
PRs: []*common.PRInfo{
|
||||
{PR: prjPR},
|
||||
{PR: pkgPR},
|
||||
},
|
||||
}
|
||||
|
||||
if res := prset.PrepareForMerge(git); res != test.expected {
|
||||
t.Errorf("Expected %v, got %v", test.expected, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRMergeMock(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(*mock_common.MockGit, *models.PullRequest, *models.PullRequest)
|
||||
config *common.AutogitConfig
|
||||
}{
|
||||
{
|
||||
name: "Success FF",
|
||||
config: &common.AutogitConfig{
|
||||
Organization: "org",
|
||||
GitProjectName: "org/_ObsPrj#master",
|
||||
MergeMode: common.MergeModeFF,
|
||||
},
|
||||
setup: func(m *mock_common.MockGit, prjPR, pkgPR *models.PullRequest) {
|
||||
m.EXPECT().GitClone("_ObsPrj", "master", prjPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("_ObsPrj", "fetch", "origin", prjPR.Head.Sha)
|
||||
m.EXPECT().GitExec("_ObsPrj", "merge", "--no-ff", "-m", gomock.Any(), prjPR.Head.Sha).Return(nil)
|
||||
|
||||
m.EXPECT().GitClone("pkg", "master", pkgPR.Base.Repo.SSHURL).Return("origin_pkg", nil)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "fetch", "origin_pkg", pkgPR.Head.Sha)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "merge", "--ff", pkgPR.Head.Sha)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "push", "origin_pkg")
|
||||
m.EXPECT().GitExecOrPanic("_ObsPrj", "push", "origin")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Success Devel",
|
||||
config: &common.AutogitConfig{
|
||||
Organization: "org",
|
||||
GitProjectName: "org/_ObsPrj#master",
|
||||
MergeMode: common.MergeModeDevel,
|
||||
},
|
||||
setup: func(m *mock_common.MockGit, prjPR, pkgPR *models.PullRequest) {
|
||||
m.EXPECT().GitClone("_ObsPrj", "master", prjPR.Base.Repo.SSHURL).Return("origin", nil)
|
||||
m.EXPECT().GitExecOrPanic("_ObsPrj", "fetch", "origin", prjPR.Head.Sha)
|
||||
m.EXPECT().GitExec("_ObsPrj", "merge", "--no-ff", "-m", gomock.Any(), prjPR.Head.Sha).Return(nil)
|
||||
|
||||
m.EXPECT().GitClone("pkg", "master", pkgPR.Base.Repo.SSHURL).Return("origin_pkg", nil)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "checkout", "-B", "master", pkgPR.Head.Sha)
|
||||
m.EXPECT().GitExecOrPanic("pkg", "push", "-f", "origin_pkg")
|
||||
m.EXPECT().GitExecOrPanic("_ObsPrj", "push", "origin")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
prjPR := &models.PullRequest{
|
||||
Index: 1,
|
||||
Base: &models.PRBranchInfo{
|
||||
Name: "master",
|
||||
Sha: "prj_base_sha",
|
||||
Repo: &models.Repository{
|
||||
Owner: &models.User{UserName: "org"},
|
||||
Name: "_ObsPrj",
|
||||
SSHURL: "ssh://git@src.opensuse.org/org/_ObsPrj.git",
|
||||
},
|
||||
},
|
||||
Head: &models.PRBranchInfo{
|
||||
Sha: "prj_head_sha",
|
||||
},
|
||||
}
|
||||
pkgPR := &models.PullRequest{
|
||||
Index: 2,
|
||||
Base: &models.PRBranchInfo{
|
||||
Name: "master",
|
||||
Sha: "pkg_base_sha",
|
||||
Repo: &models.Repository{
|
||||
Owner: &models.User{UserName: "org"},
|
||||
Name: "pkg",
|
||||
SSHURL: "ssh://git@src.opensuse.org/org/pkg.git",
|
||||
},
|
||||
},
|
||||
Head: &models.PRBranchInfo{
|
||||
Sha: "pkg_head_sha",
|
||||
},
|
||||
}
|
||||
|
||||
ctl := gomock.NewController(t)
|
||||
git := mock_common.NewMockGit(ctl)
|
||||
reviewUnrequestMock := mock_common.NewMockGiteaReviewUnrequester(ctl)
|
||||
reviewUnrequestMock.EXPECT().UnrequestReview(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
|
||||
test.setup(git, prjPR, pkgPR)
|
||||
|
||||
prset := &common.PRSet{
|
||||
Config: test.config,
|
||||
PRs: []*common.PRInfo{
|
||||
{PR: prjPR},
|
||||
{PR: pkgPR},
|
||||
},
|
||||
}
|
||||
|
||||
if err := prset.Merge(reviewUnrequestMock, git); err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRAddMergeCommit(t *testing.T) {
|
||||
pkgPR := &models.PullRequest{
|
||||
Index: 2,
|
||||
Base: &models.PRBranchInfo{
|
||||
Name: "master",
|
||||
Sha: "pkg_base_sha",
|
||||
Repo: &models.Repository{
|
||||
Owner: &models.User{UserName: "org"},
|
||||
Name: "pkg",
|
||||
SSHURL: "ssh://git@src.opensuse.org/org/pkg.git",
|
||||
},
|
||||
},
|
||||
Head: &models.PRBranchInfo{
|
||||
Name: "branch_name",
|
||||
Sha: "pkg_head_sha",
|
||||
},
|
||||
AllowMaintainerEdit: true,
|
||||
}
|
||||
|
||||
config := &common.AutogitConfig{
|
||||
Organization: "org",
|
||||
GitProjectName: "org/_ObsPrj#master",
|
||||
MergeMode: common.MergeModeReplace,
|
||||
}
|
||||
|
||||
ctl := gomock.NewController(t)
|
||||
git := mock_common.NewMockGit(ctl)
|
||||
|
||||
git.EXPECT().GitExec("pkg", "merge", "--no-ff", "--no-commit", "-X", "theirs", pkgPR.Head.Sha).Return(nil)
|
||||
git.EXPECT().GitExecOrPanic("pkg", "read-tree", "--reset", "-u", pkgPR.Head.Sha)
|
||||
git.EXPECT().GitExecOrPanic("pkg", "commit", "-m", gomock.Any())
|
||||
git.EXPECT().GitExecOrPanic("pkg", "push", "origin", "HEAD:branch_name")
|
||||
git.EXPECT().GitExecWithOutputOrPanic("pkg", "rev-list", "-1", "HEAD").Return("new_head_sha")
|
||||
|
||||
prset := &common.PRSet{
|
||||
Config: config,
|
||||
PRs: []*common.PRInfo{
|
||||
{PR: &models.PullRequest{}}, // prjgit at index 0
|
||||
{PR: pkgPR}, // pkg at index 1
|
||||
},
|
||||
}
|
||||
|
||||
if res := prset.AddMergeCommit(git, "origin", 1); !res {
|
||||
t.Errorf("Expected true, got %v", res)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,13 +92,10 @@ func ConnectToExchangeForPublish(host, username, password string) {
|
||||
auth = username + ":" + password + "@"
|
||||
}
|
||||
|
||||
connection, err := rabbitmq.DialConfig("amqps://"+auth+host, rabbitmq.Config{
|
||||
Dial: rabbitmq.DefaultDial(10 * time.Second),
|
||||
TLSClientConfig: &tls.Config{
|
||||
ServerName: host,
|
||||
},
|
||||
connection, err := rabbitmq.DialTLS("amqps://"+auth+host, &tls.Config{
|
||||
ServerName: host,
|
||||
})
|
||||
failOnError(err, "Cannot connect to "+host)
|
||||
failOnError(err, "Cannot connect to rabbit.opensuse.org")
|
||||
defer connection.Close()
|
||||
|
||||
ch, err := connection.Channel()
|
||||
|
||||
@@ -7,8 +7,4 @@ markers =
|
||||
t005: Test case 005
|
||||
t006: Test case 006
|
||||
t007: Test case 007
|
||||
t008: Test case 008
|
||||
t009: Test case 009
|
||||
t010: Test case 010
|
||||
t011: Test case 011
|
||||
dependency: pytest-dependency marker
|
||||
|
||||
@@ -76,10 +76,6 @@ The testing will be conducted in a dedicated test environment that mimics the pr
|
||||
| **TC-MERGE-005** | - | **ManualMergeOnly with Project 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 project maintainer. | 1. The PR is merged. | High |
|
||||
| **TC-MERGE-006** | - | **ManualMergeProject with Project Maintainer** | 1. Create a PackageGit PR with `ManualMergeProject` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on the project PR from the account of a project maintainer. | 1. The PR is merged. | High |
|
||||
| **TC-MERGE-007** | - | **ManualMergeProject with unauthorized user** | 1. Create a PackageGit PR with `ManualMergeProject` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on the project PR from the account of a package maintainer. | 1. The PR is not merged. | High |
|
||||
| **TC-MERGE-008** | P | **MergeMode: ff-only (Success)** | 1. Set `MergeMode = "ff-only"`.<br>2. Create a FF-mergeable PackageGit PR.<br>3. Approve reviews on both PRs. | 1. Both PRs are automatically merged successfully. | High |
|
||||
| **TC-MERGE-009** | P | **MergeMode: ff-only (Failure)** | 1. Set `MergeMode = "ff-only"`.<br>2. Create a PackageGit PR that adds a new file.<br>3. Commit the same file with different content to the base branch to create a content conflict.<br>4. Approve reviews and trigger a sync by pushing another change. | 1. The bot detects it is not FF-mergeable.<br>2. The PR is NOT merged. | High |
|
||||
| **TC-MERGE-010** | P | **MergeMode: devel (Force-push)** | 1. Set `MergeMode = "devel"`.<br>2. Create a PackageGit PR that adds a new file.<br>3. Commit the same file with different content to the base branch to create a content conflict.<br>4. Approve reviews. | 1. Both PRs are merged.<br>2. The `pkgA` submodule points to the PR's head SHA. | High |
|
||||
| **TC-MERGE-011** | P | **MergeMode: replace (Merge-commit)** | 1. Set `MergeMode = "replace"`.<br>2. Create a PackageGit PR that adds a new file.<br>3. Enable "Allow edits from maintainers" on the PR.<br>4. Commit the same file with different content to the base branch to create a content conflict.<br>5. Approve reviews. | 1. Both PRs are merged.<br>2. The project branch HEAD is a merge commit (has >1 parent).<br>3. The `pkgA` submodule points to the PR's head SHA. | High |
|
||||
| **TC-CONFIG-001** | - | **Invalid Configuration** | 1. Provide an invalid `workflow.config` file. | 1. The bot reports an error and does not process any PRs. | High |
|
||||
| **TC-LABEL-001** | P | **Apply `staging/Auto` label** | 1. Create a new PackageGit PR. | 1. The `staging/Auto` label is applied to the ProjectGit PR. | High |
|
||||
| **TC-LABEL-002** | x | **Apply `review/Pending` label** | 1. Create a new PackageGit PR. | 1. The `review/Pending` label is applied to the ProjectGit PR when there are pending reviews. | Medium |
|
||||
|
||||
@@ -71,21 +71,6 @@ BRANCH_CONFIG_CUSTOM = {
|
||||
"ReviewPending": "review/Pending"
|
||||
}
|
||||
}
|
||||
},
|
||||
"merge-ff": {
|
||||
"workflow.config": {
|
||||
"MergeMode": "ff-only"
|
||||
}
|
||||
},
|
||||
"merge-replace": {
|
||||
"workflow.config": {
|
||||
"MergeMode": "replace"
|
||||
}
|
||||
},
|
||||
"merge-devel": {
|
||||
"workflow.config": {
|
||||
"MergeMode": "devel"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,18 +275,6 @@ def no_project_git_pr_env(gitea_env):
|
||||
def label_env(gitea_env):
|
||||
return gitea_env, "myproducts/mySLFO", "label-test"
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def merge_ff_env(gitea_env):
|
||||
return gitea_env, "myproducts/mySLFO", "merge-ff"
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def merge_replace_env(gitea_env):
|
||||
return gitea_env, "myproducts/mySLFO", "merge-replace"
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def merge_devel_env(gitea_env):
|
||||
return gitea_env, "myproducts/mySLFO", "merge-devel"
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def usera_client(gitea_env):
|
||||
return GiteaAPIClient(base_url=gitea_env.base_url, token=gitea_env.headers["Authorization"].split(" ")[1], sudo="usera")
|
||||
|
||||
@@ -3,7 +3,6 @@ import time
|
||||
import pytest
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
import base64
|
||||
@@ -531,45 +530,3 @@ index 0000000..{pkg_b_sha}
|
||||
print(f"Error restarting service {service_name}: {e}")
|
||||
raise
|
||||
|
||||
def wait_for_project_pr(self, package_pr_repo, package_pr_number, project_pr_repo="myproducts/mySLFO", timeout=60):
|
||||
print(f"Polling {package_pr_repo} PR #{package_pr_number} timeline for forwarded PR event in {project_pr_repo}...")
|
||||
for _ in range(timeout):
|
||||
time.sleep(1)
|
||||
timeline_events = self.get_timeline_events(package_pr_repo, package_pr_number)
|
||||
for event in timeline_events:
|
||||
if event.get("type") == "pull_ref":
|
||||
if not (ref_issue := event.get("ref_issue")):
|
||||
continue
|
||||
url_to_check = ref_issue.get("html_url", "")
|
||||
match = re.search(fr"{project_pr_repo}/pulls/(\d+)", url_to_check)
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
return None
|
||||
|
||||
def approve_and_wait_merge(self, package_pr_repo, package_pr_number, project_pr_number, project_pr_repo="myproducts/mySLFO", timeout=30):
|
||||
print(f"Approving reviews and verifying both PRs are merged ({package_pr_repo}#{package_pr_number} and {project_pr_repo}#{project_pr_number})...")
|
||||
package_merged = False
|
||||
project_merged = False
|
||||
|
||||
for i in range(timeout):
|
||||
self.approve_requested_reviews(package_pr_repo, package_pr_number)
|
||||
self.approve_requested_reviews(project_pr_repo, project_pr_number)
|
||||
|
||||
if not package_merged:
|
||||
pkg_details = self.get_pr_details(package_pr_repo, package_pr_number)
|
||||
if pkg_details.get("merged"):
|
||||
package_merged = True
|
||||
print(f"Package PR {package_pr_repo}#{package_pr_number} merged.")
|
||||
|
||||
if not project_merged:
|
||||
prj_details = self.get_pr_details(project_pr_repo, project_pr_number)
|
||||
if prj_details.get("merged"):
|
||||
project_merged = True
|
||||
print(f"Project PR {project_pr_repo}#{project_pr_number} merged.")
|
||||
|
||||
if package_merged and project_merged:
|
||||
return True, True
|
||||
|
||||
time.sleep(1)
|
||||
return package_merged, project_merged
|
||||
|
||||
|
||||
@@ -23,10 +23,27 @@ def test_pr_workflow_succeeded(staging_main_env, mock_build_result):
|
||||
|
||||
compose_dir = Path(__file__).parent.parent
|
||||
|
||||
forwarded_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", initial_pr_number)
|
||||
forwarded_pr_number = None
|
||||
print(
|
||||
f"Polling mypool/pkgA PR #{initial_pr_number} timeline for forwarded PR event..."
|
||||
)
|
||||
for _ in range(20):
|
||||
time.sleep(1)
|
||||
timeline_events = gitea_env.get_timeline_events("mypool/pkgA", initial_pr_number)
|
||||
for event in timeline_events:
|
||||
if event.get("type") == "pull_ref":
|
||||
if not (ref_issue := event.get("ref_issue")):
|
||||
continue
|
||||
url_to_check = ref_issue.get("html_url", "")
|
||||
match = re.search(r"myproducts/mySLFO/pulls/(\d+)", url_to_check)
|
||||
if match:
|
||||
forwarded_pr_number = match.group(1)
|
||||
break
|
||||
if forwarded_pr_number:
|
||||
break
|
||||
assert (
|
||||
forwarded_pr_number is not None
|
||||
), "Workflow bot did not create a project PR."
|
||||
), "Workflow bot did not create a pull_ref event on the timeline."
|
||||
print(f"Found forwarded PR: myproducts/mySLFO #{forwarded_pr_number}")
|
||||
|
||||
print(f"Polling myproducts/mySLFO PR #{forwarded_pr_number} for reviewer assignment...")
|
||||
@@ -77,10 +94,27 @@ def test_pr_workflow_failed(staging_main_env, mock_build_result):
|
||||
|
||||
compose_dir = Path(__file__).parent.parent
|
||||
|
||||
forwarded_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", initial_pr_number)
|
||||
forwarded_pr_number = None
|
||||
print(
|
||||
f"Polling mypool/pkgA PR #{initial_pr_number} timeline for forwarded PR event..."
|
||||
)
|
||||
for _ in range(20):
|
||||
time.sleep(1)
|
||||
timeline_events = gitea_env.get_timeline_events("mypool/pkgA", initial_pr_number)
|
||||
for event in timeline_events:
|
||||
if event.get("type") == "pull_ref":
|
||||
if not (ref_issue := event.get("ref_issue")):
|
||||
continue
|
||||
url_to_check = ref_issue.get("html_url", "")
|
||||
match = re.search(r"myproducts/mySLFO/pulls/(\d+)", url_to_check)
|
||||
if match:
|
||||
forwarded_pr_number = match.group(1)
|
||||
break
|
||||
if forwarded_pr_number:
|
||||
break
|
||||
assert (
|
||||
forwarded_pr_number is not None
|
||||
), "Workflow bot did not create a project PR."
|
||||
), "Workflow bot did not create a pull_ref event on the timeline."
|
||||
print(f"Found forwarded PR: myproducts/mySLFO #{forwarded_pr_number}")
|
||||
|
||||
print(f"Polling myproducts/mySLFO PR #{forwarded_pr_number} for reviewer assignment...")
|
||||
|
||||
@@ -35,7 +35,23 @@ index 0000000..e69de29
|
||||
print(f"Created package PR mypool/pkgA#{package_pr_number}")
|
||||
|
||||
# 2. Make sure the workflow-pr service created related project PR
|
||||
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", package_pr_number)
|
||||
project_pr_number = None
|
||||
print(f"Polling mypool/pkgA PR #{package_pr_number} timeline for forwarded PR event...")
|
||||
for _ in range(40):
|
||||
time.sleep(1)
|
||||
timeline_events = gitea_env.get_timeline_events("mypool/pkgA", package_pr_number)
|
||||
for event in timeline_events:
|
||||
if event.get("type") == "pull_ref":
|
||||
if not (ref_issue := event.get("ref_issue")):
|
||||
continue
|
||||
url_to_check = ref_issue.get("html_url", "")
|
||||
match = re.search(r"myproducts/mySLFO/pulls/(\d+)", url_to_check)
|
||||
if match:
|
||||
project_pr_number = int(match.group(1))
|
||||
break
|
||||
if project_pr_number:
|
||||
break
|
||||
|
||||
assert project_pr_number is not None, "Workflow bot did not create a project PR."
|
||||
print(f"Found project PR: myproducts/mySLFO#{project_pr_number}")
|
||||
|
||||
|
||||
@@ -25,14 +25,55 @@ index 0000000..e69de29
|
||||
print(f"Created package PR mypool/pkgA#{package_pr_number}")
|
||||
|
||||
# 2. Make sure the workflow-pr service created related project PR
|
||||
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", package_pr_number)
|
||||
project_pr_number = None
|
||||
print(f"Polling mypool/pkgA PR #{package_pr_number} timeline for forwarded PR event...")
|
||||
for _ in range(40):
|
||||
time.sleep(1)
|
||||
timeline_events = gitea_env.get_timeline_events("mypool/pkgA", package_pr_number)
|
||||
for event in timeline_events:
|
||||
if event.get("type") == "pull_ref":
|
||||
if not (ref_issue := event.get("ref_issue")):
|
||||
continue
|
||||
url_to_check = ref_issue.get("html_url", "")
|
||||
match = re.search(r"myproducts/mySLFO/pulls/(\d+)", url_to_check)
|
||||
if match:
|
||||
project_pr_number = int(match.group(1))
|
||||
break
|
||||
if project_pr_number:
|
||||
break
|
||||
|
||||
assert project_pr_number is not None, "Workflow bot did not create a project PR."
|
||||
print(f"Found project PR: myproducts/mySLFO#{project_pr_number}")
|
||||
|
||||
# 3. Approve reviews and verify merged
|
||||
pkg_merged, prj_merged = gitea_env.approve_and_wait_merge("mypool/pkgA", package_pr_number, project_pr_number)
|
||||
assert pkg_merged, f"Package PR mypool/pkgA#{package_pr_number} was not merged automatically."
|
||||
assert prj_merged, f"Project PR myproducts/mySLFO#{project_pr_number} was not merged automatically."
|
||||
print("Approving reviews and verifying both PRs are merged...")
|
||||
package_merged = False
|
||||
project_merged = False
|
||||
|
||||
for i in range(20): # Poll for up to 20 seconds
|
||||
gitea_env.approve_requested_reviews("mypool/pkgA", package_pr_number)
|
||||
gitea_env.approve_requested_reviews("myproducts/mySLFO", project_pr_number)
|
||||
|
||||
if not package_merged:
|
||||
pkg_details = gitea_env.get_pr_details("mypool/pkgA", package_pr_number)
|
||||
if pkg_details.get("merged"):
|
||||
package_merged = True
|
||||
print(f"Package PR mypool/pkgA#{package_pr_number} merged.")
|
||||
|
||||
# Project PR
|
||||
if not project_merged:
|
||||
prj_details = gitea_env.get_pr_details("myproducts/mySLFO", project_pr_number)
|
||||
if prj_details.get("merged"):
|
||||
project_merged = True
|
||||
print(f"Project PR myproducts/mySLFO#{project_pr_number} merged.")
|
||||
|
||||
if package_merged and project_merged:
|
||||
break
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
assert package_merged, f"Package PR mypool/pkgA#{package_pr_number} was not merged automatically."
|
||||
assert project_merged, f"Project PR myproducts/mySLFO#{project_pr_number} was not merged automatically."
|
||||
print("Both PRs merged successfully.")
|
||||
|
||||
@pytest.mark.t002
|
||||
@@ -57,7 +98,23 @@ index 0000000..e69de29
|
||||
print(f"Created package PR mypool/pkgA#{package_pr_number}")
|
||||
|
||||
# 2. Make sure the workflow-pr service created related project PR
|
||||
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", package_pr_number)
|
||||
project_pr_number = None
|
||||
print(f"Polling mypool/pkgA PR #{package_pr_number} timeline for forwarded PR event...")
|
||||
for _ in range(40):
|
||||
time.sleep(1)
|
||||
timeline_events = gitea_env.get_timeline_events("mypool/pkgA", package_pr_number)
|
||||
for event in timeline_events:
|
||||
if event.get("type") == "pull_ref":
|
||||
if not (ref_issue := event.get("ref_issue")):
|
||||
continue
|
||||
url_to_check = ref_issue.get("html_url", "")
|
||||
match = re.search(r"myproducts/mySLFO/pulls/(\d+)", url_to_check)
|
||||
if match:
|
||||
project_pr_number = int(match.group(1))
|
||||
break
|
||||
if project_pr_number:
|
||||
break
|
||||
|
||||
assert project_pr_number is not None, "Workflow bot did not create a project PR."
|
||||
print(f"Found project PR: myproducts/mySLFO#{project_pr_number}")
|
||||
|
||||
@@ -136,172 +193,3 @@ index 0000000..e69de29
|
||||
assert package_merged, f"Package PR mypool/pkgA#{package_pr_number} was not merged after 'merge ok'."
|
||||
assert project_merged, f"Project PR myproducts/mySLFO#{project_pr_number} was not merged after 'merge ok'."
|
||||
print("Both PRs merged successfully after 'merge ok'.")
|
||||
|
||||
@pytest.mark.t008
|
||||
def test_008_merge_mode_ff_only_success(merge_ff_env, test_user_client):
|
||||
"""
|
||||
Test MergeMode "ff-only" - Success case (FF-mergeable)
|
||||
"""
|
||||
gitea_env, test_full_repo_name, merge_branch_name = merge_ff_env
|
||||
|
||||
# 1. Create a package PR (this will be FF-mergeable by default)
|
||||
diff = """diff --git a/ff_test.txt b/ff_test.txt
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
"""
|
||||
package_pr = test_user_client.create_gitea_pr("mypool/pkgA", diff, "Test FF Merge", False, base_branch=merge_branch_name)
|
||||
package_pr_number = package_pr["number"]
|
||||
|
||||
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", package_pr_number)
|
||||
assert project_pr_number is not None
|
||||
|
||||
pkg_merged, prj_merged = gitea_env.approve_and_wait_merge("mypool/pkgA", package_pr_number, project_pr_number)
|
||||
assert pkg_merged and prj_merged
|
||||
|
||||
@pytest.mark.t009
|
||||
def test_009_merge_mode_ff_only_failure(merge_ff_env, ownerA_client):
|
||||
"""
|
||||
Test MergeMode "ff-only" - Failure case (Content Conflict, should NOT merge)
|
||||
"""
|
||||
gitea_env, test_full_repo_name, merge_branch_name = merge_ff_env
|
||||
|
||||
ts = time.strftime("%H%M%S")
|
||||
filename = f"ff_fail_test_{ts}.txt"
|
||||
|
||||
# 1. Create a package PR that adds a file
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
--- /dev/null
|
||||
+++ b/{filename}
|
||||
@@ -0,0 +1 @@
|
||||
+PR content
|
||||
"""
|
||||
package_pr = ownerA_client.create_gitea_pr("mypool/pkgA", diff, "Test FF Merge Failure (Conflict)", False, base_branch=merge_branch_name)
|
||||
package_pr_number = package_pr["number"]
|
||||
|
||||
# 2. Wait for project PR to be created
|
||||
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", package_pr_number)
|
||||
assert project_pr_number is not None
|
||||
|
||||
print("Making PR non-FF by creating a content conflict in the base branch...")
|
||||
gitea_env.create_file("mypool", "pkgA", filename, "Conflicting base content\n", branch=merge_branch_name)
|
||||
|
||||
print("Approving reviews initially...")
|
||||
gitea_env.approve_requested_reviews("mypool/pkgA", package_pr_number)
|
||||
gitea_env.approve_requested_reviews("myproducts/mySLFO", project_pr_number)
|
||||
|
||||
print("Pushing another change to PR branch to trigger sync...")
|
||||
gitea_env.modify_gitea_pr("mypool/pkgA", package_pr_number,
|
||||
"diff --git a/sync_test.txt b/sync_test.txt\nnew file mode 100644\nindex 0000000..e69de29\n",
|
||||
"Trigger Sync")
|
||||
|
||||
# The bot should detect it's not FF and NOT merge, and re-request reviews because of the new commit
|
||||
print("Waiting for reviews to be re-requested and approving again...")
|
||||
time.sleep(10) # Wait for bot to process sync
|
||||
|
||||
# Approve again and verify it is NOT merged
|
||||
print("Approving again and verifying PR is NOT merged (because it's not FF)...")
|
||||
for i in range(15):
|
||||
gitea_env.approve_requested_reviews("mypool/pkgA", package_pr_number)
|
||||
gitea_env.approve_requested_reviews("myproducts/mySLFO", project_pr_number)
|
||||
time.sleep(1)
|
||||
|
||||
pkg_details = gitea_env.get_pr_details("mypool/pkgA", package_pr_number)
|
||||
assert not pkg_details.get("merged"), "Package PR merged despite NOT being FF-mergeable!"
|
||||
|
||||
print("FF-only failure (not merged after sync) verified.")
|
||||
|
||||
@pytest.mark.t010
|
||||
def test_010_merge_mode_devel_success(merge_devel_env, ownerA_client):
|
||||
"""
|
||||
Test MergeMode "devel" - Success case (Content Conflict, should still merge via force-push)
|
||||
"""
|
||||
gitea_env, test_full_repo_name, merge_branch_name = merge_devel_env
|
||||
|
||||
ts = time.strftime("%H%M%S")
|
||||
filename = f"devel_test_{ts}.txt"
|
||||
|
||||
# 1. Create a package PR that adds a file
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
--- /dev/null
|
||||
+++ b/{filename}
|
||||
@@ -0,0 +1 @@
|
||||
+PR content
|
||||
"""
|
||||
package_pr = ownerA_client.create_gitea_pr("mypool/pkgA", diff, "Test Devel Merge (Conflict)", False, base_branch=merge_branch_name)
|
||||
package_pr_number = package_pr["number"]
|
||||
|
||||
# 2. Create a content conflict by committing the same file to the base branch
|
||||
gitea_env.create_file("mypool", "pkgA", filename, "Conflicting base content\n", branch=merge_branch_name)
|
||||
|
||||
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", package_pr_number)
|
||||
assert project_pr_number is not None
|
||||
|
||||
# Before merge, get the head sha of the package pr and project pr
|
||||
pkg_details = gitea_env.get_pr_details("mypool/pkgA", package_pr_number)
|
||||
pkg_head_sha = pkg_details["head"]["sha"]
|
||||
prj_details = gitea_env.get_pr_details("myproducts/mySLFO", project_pr_number)
|
||||
prj_head_sha = prj_details["head"]["sha"]
|
||||
|
||||
pkg_merged, prj_merged = gitea_env.approve_and_wait_merge("mypool/pkgA", package_pr_number, project_pr_number)
|
||||
assert pkg_merged and prj_merged
|
||||
print("Devel merge (force-push) successful.")
|
||||
|
||||
# Verify that pkgA submodule points to the correct SHA
|
||||
pkgA_submodule_info = gitea_env.get_file_info("myproducts", "mySLFO", "pkgA", branch=merge_branch_name)
|
||||
assert pkgA_submodule_info["sha"] == pkg_head_sha, f"Submodule pkgA should point to {pkg_head_sha} but points to {pkgA_submodule_info['sha']}"
|
||||
|
||||
@pytest.mark.t011
|
||||
def test_011_merge_mode_replace_success(merge_replace_env, ownerA_client):
|
||||
"""
|
||||
Test MergeMode "replace" - Success case (Content Conflict, bot should add merge commit)
|
||||
"""
|
||||
gitea_env, test_full_repo_name, merge_branch_name = merge_replace_env
|
||||
|
||||
ts = time.strftime("%H%M%S")
|
||||
filename = f"replace_test_{ts}.txt"
|
||||
|
||||
# 1. Create a package PR that adds a file
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
--- /dev/null
|
||||
+++ b/{filename}
|
||||
@@ -0,0 +1 @@
|
||||
+PR content
|
||||
"""
|
||||
package_pr = ownerA_client.create_gitea_pr("mypool/pkgA", diff, "Test Replace Merge (Conflict)", False, base_branch=merge_branch_name)
|
||||
package_pr_number = package_pr["number"]
|
||||
|
||||
# Enable "Allow edits from maintainers"
|
||||
ownerA_client.update_gitea_pr_properties("mypool/pkgA", package_pr_number, allow_maintainer_edit=True)
|
||||
|
||||
# 2. Create a content conflict by committing the same file to the base branch
|
||||
gitea_env.create_file("mypool", "pkgA", filename, "Conflicting base content\n", branch=merge_branch_name)
|
||||
|
||||
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", package_pr_number)
|
||||
assert project_pr_number is not None
|
||||
|
||||
# Before merge, get the head sha of the package pr and project pr
|
||||
pkg_details = gitea_env.get_pr_details("mypool/pkgA", package_pr_number)
|
||||
pkg_head_sha = pkg_details["head"]["sha"]
|
||||
prj_details = gitea_env.get_pr_details("myproducts/mySLFO", project_pr_number)
|
||||
prj_head_sha = prj_details["head"]["sha"]
|
||||
|
||||
pkg_merged, prj_merged = gitea_env.approve_and_wait_merge("mypool/pkgA", package_pr_number, project_pr_number, timeout=60)
|
||||
assert pkg_merged and prj_merged
|
||||
print("Replace merge successful.")
|
||||
|
||||
# Verify that the project branch HEAD is a merge commit
|
||||
branch_info = gitea_env._request("GET", f"repos/myproducts/mySLFO/branches/{merge_branch_name}").json()
|
||||
new_head_sha = branch_info["commit"]["id"]
|
||||
|
||||
commit_details = gitea_env._request("GET", f"repos/myproducts/mySLFO/git/commits/{new_head_sha}").json()
|
||||
assert len(commit_details["parents"]) > 1, f"Project branch {merge_branch_name} HEAD should be a merge commit but has {len(commit_details['parents'])} parents"
|
||||
|
||||
# Verify that pkgA submodule points to the correct SHA
|
||||
pkgA_submodule_info = gitea_env.get_file_info("myproducts", "mySLFO", "pkgA", branch=merge_branch_name)
|
||||
assert pkgA_submodule_info["sha"] == pkg_head_sha, f"Submodule pkgA should point to {pkg_head_sha} but points to {pkgA_submodule_info['sha']}"
|
||||
|
||||
@@ -94,7 +94,23 @@ index 0000000..e69de29
|
||||
print(f"Created package PR mypool/pkgA#{package_pr_number}")
|
||||
|
||||
# 2. Make sure the workflow-pr service created related project PR
|
||||
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", package_pr_number)
|
||||
project_pr_number = None
|
||||
print(f"Polling mypool/pkgA PR #{package_pr_number} timeline for forwarded PR event...")
|
||||
for _ in range(40):
|
||||
time.sleep(1)
|
||||
timeline_events = gitea_env.get_timeline_events("mypool/pkgA", package_pr_number)
|
||||
for event in timeline_events:
|
||||
if event.get("type") == "pull_ref":
|
||||
if not (ref_issue := event.get("ref_issue")):
|
||||
continue
|
||||
url_to_check = ref_issue.get("html_url", "")
|
||||
match = re.search(r"myproducts/mySLFO/pulls/(\d+)", url_to_check)
|
||||
if match:
|
||||
project_pr_number = int(match.group(1))
|
||||
break
|
||||
if project_pr_number:
|
||||
break
|
||||
|
||||
assert project_pr_number is not None, "Workflow bot did not create a project PR."
|
||||
print(f"Found project PR: myproducts/mySLFO#{project_pr_number}")
|
||||
|
||||
@@ -163,7 +179,23 @@ index 0000000..e69de29
|
||||
print(f"Created package PR mypool/pkgB#{package_pr_number}")
|
||||
|
||||
# 2. Make sure the workflow-pr service created related project PR
|
||||
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgB", package_pr_number)
|
||||
project_pr_number = None
|
||||
print(f"Polling mypool/pkgB PR #{package_pr_number} timeline for forwarded PR event...")
|
||||
for _ in range(40):
|
||||
time.sleep(1)
|
||||
timeline_events = gitea_env.get_timeline_events("mypool/pkgB", package_pr_number)
|
||||
for event in timeline_events:
|
||||
if event.get("type") == "pull_ref":
|
||||
if not (ref_issue := event.get("ref_issue")):
|
||||
continue
|
||||
url_to_check = ref_issue.get("html_url", "")
|
||||
match = re.search(r"myproducts/mySLFO/pulls/(\d+)", url_to_check)
|
||||
if match:
|
||||
project_pr_number = int(match.group(1))
|
||||
break
|
||||
if project_pr_number:
|
||||
break
|
||||
|
||||
assert project_pr_number is not None, "Workflow bot did not create a project PR."
|
||||
print(f"Found project PR: myproducts/mySLFO#{project_pr_number}")
|
||||
|
||||
@@ -285,7 +317,23 @@ index 0000000..e69de29
|
||||
print(f"Created package PR mypool/pkgB#{package_pr_number}")
|
||||
|
||||
# 2. Make sure the workflow-pr service created related project PR
|
||||
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgB", package_pr_number)
|
||||
project_pr_number = None
|
||||
print(f"Polling mypool/pkgB PR #{package_pr_number} timeline for forwarded PR event...")
|
||||
for _ in range(40):
|
||||
time.sleep(1)
|
||||
timeline_events = gitea_env.get_timeline_events("mypool/pkgB", package_pr_number)
|
||||
for event in timeline_events:
|
||||
if event.get("type") == "pull_ref":
|
||||
if not (ref_issue := event.get("ref_issue")):
|
||||
continue
|
||||
url_to_check = ref_issue.get("html_url", "")
|
||||
match = re.search(r"myproducts/mySLFO/pulls/(\d+)", url_to_check)
|
||||
if match:
|
||||
project_pr_number = int(match.group(1))
|
||||
break
|
||||
if project_pr_number:
|
||||
break
|
||||
|
||||
assert project_pr_number is not None, "Workflow bot did not create a project PR."
|
||||
print(f"Found project PR: myproducts/mySLFO#{project_pr_number}")
|
||||
|
||||
|
||||
@@ -27,8 +27,27 @@ def test_001_project_pr(gitea_env):
|
||||
pytest.initial_pr_number = pytest.pr["number"]
|
||||
time.sleep(5) # Give Gitea some time to process the PR and make the timeline available
|
||||
|
||||
pytest.forwarded_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", pytest.initial_pr_number)
|
||||
|
||||
compose_dir = Path(__file__).parent.parent
|
||||
|
||||
pytest.forwarded_pr_number = None
|
||||
print(
|
||||
f"Polling mypool/pkgA PR #{pytest.initial_pr_number} timeline for forwarded PR event..."
|
||||
)
|
||||
# Instead of polling timeline, check if forwarded PR exists directly
|
||||
for _ in range(20):
|
||||
time.sleep(1)
|
||||
timeline_events = gitea_env.get_timeline_events("mypool/pkgA", pytest.initial_pr_number)
|
||||
for event in timeline_events:
|
||||
if event.get("type") == "pull_ref":
|
||||
if not (ref_issue := event.get("ref_issue")):
|
||||
continue
|
||||
url_to_check = ref_issue.get("html_url", "")
|
||||
match = re.search(r"myproducts/mySLFO/pulls/(\d+)", url_to_check)
|
||||
if match:
|
||||
pytest.forwarded_pr_number = match.group(1)
|
||||
break
|
||||
if pytest.forwarded_pr_number:
|
||||
break
|
||||
assert (
|
||||
pytest.forwarded_pr_number is not None
|
||||
), "Workflow bot did not create a forwarded PR."
|
||||
@@ -125,9 +144,23 @@ index 0000000..e69de29
|
||||
print(f"Created Package PR #{package_pr_number}")
|
||||
|
||||
# 2. Verify that the workflow-pr bot did not create a Project PR
|
||||
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", package_pr_number, timeout=10)
|
||||
project_pr_created = False
|
||||
for i in range(10): # Poll for some time
|
||||
time.sleep(2)
|
||||
timeline_events = gitea_env.get_timeline_events("mypool/pkgA", package_pr_number)
|
||||
for event in timeline_events:
|
||||
if event.get("type") == "pull_ref":
|
||||
if not (ref_issue := event.get("ref_issue")):
|
||||
continue
|
||||
url_to_check = ref_issue.get("html_url", "")
|
||||
match = re.search(r"myproducts/mySLFO/pulls/(\d+)", url_to_check)
|
||||
if match:
|
||||
project_pr_created = True
|
||||
break
|
||||
if project_pr_created:
|
||||
break
|
||||
|
||||
assert project_pr_number is None, "Workflow bot unexpectedly created a Project PR in myproducts/mySLFO."
|
||||
assert not project_pr_created, "Workflow bot unexpectedly created a Project PR in myproducts/mySLFO."
|
||||
print("Verification complete: No Project PR was created by the bot.")
|
||||
|
||||
# 3. Manually create the Project PR
|
||||
@@ -219,9 +252,24 @@ index 0000000..e69de29
|
||||
pkgA_pr_head_sha = package_pr_details["head"]["sha"]
|
||||
|
||||
# 3. Assert that the workflow-pr bot did not create a Project PR in the myproducts/mySLFO repository
|
||||
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", package_pr_number, timeout=10)
|
||||
project_pr_created = False
|
||||
for i in range(20): # Poll for a reasonable time
|
||||
time.sleep(2) # Wait a bit longer to be sure
|
||||
timeline_events = gitea_env.get_timeline_events("mypool/pkgA", package_pr_number)
|
||||
for event in timeline_events:
|
||||
if event.get("type") == "pull_ref":
|
||||
if not (ref_issue := event.get("ref_issue")):
|
||||
continue
|
||||
url_to_check = ref_issue.get("html_url", "")
|
||||
# Regex now searches for myproducts/mySLFO/pulls/(\d+)
|
||||
match = re.search(r"myproducts/mySLFO/pulls/(\d+)", url_to_check)
|
||||
if match:
|
||||
project_pr_created = True
|
||||
break
|
||||
if project_pr_created:
|
||||
break
|
||||
|
||||
assert project_pr_number is None, "Workflow bot unexpectedly created a Project PR in myproducts/mySLFO."
|
||||
assert not project_pr_created, "Workflow bot unexpectedly created a Project PR in myproducts/mySLFO."
|
||||
print("Verification complete: No Project PR was created in myproducts/mySLFO as expected.")
|
||||
|
||||
# 1. Create that Project PR from the test code.
|
||||
@@ -274,3 +322,5 @@ index 0000000..f587a12
|
||||
|
||||
assert project_pr_updated, "Manually created Project PR was not updated by the bot."
|
||||
print("Verification complete: Manually created Project PR was updated by the bot as expected.")
|
||||
|
||||
|
||||
|
||||
@@ -6,8 +6,5 @@
|
||||
"myproducts/mySLFO#maintainer-merge",
|
||||
"myproducts/mySLFO#review-required",
|
||||
"myproducts/mySLFO#label-test",
|
||||
"myproducts/mySLFO#manual-merge",
|
||||
"myproducts/mySLFO#merge-ff",
|
||||
"myproducts/mySLFO#merge-replace",
|
||||
"myproducts/mySLFO#merge-devel"
|
||||
"myproducts/mySLFO#manual-merge"
|
||||
]
|
||||
|
||||
45
pr-status-service/README.md
Normal file
45
pr-status-service/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
OBS Status Service
|
||||
==================
|
||||
|
||||
Caches and reports status of a PR as SVG or JSON
|
||||
|
||||
Requests for individual PRs statuses:
|
||||
GET /${PR_HASH}
|
||||
|
||||
Update requests for individual PRs statuses:
|
||||
POST /${PR_HASH}
|
||||
|
||||
POST requires cert auth to function.
|
||||
|
||||
|
||||
Areas of Responsibility
|
||||
-----------------------
|
||||
|
||||
* Listens for PR status reports from workflow-pr bot (or other interface)
|
||||
* Produces SVG output based on GET request
|
||||
* Produces JSON output based on GET request
|
||||
|
||||
|
||||
Target Usage
|
||||
------------
|
||||
|
||||
* comment section of a PR
|
||||
* 3rd party tooling
|
||||
|
||||
|
||||
PR Encoding
|
||||
-----------
|
||||
|
||||
PRs are encoded as SHA256 hashes with a salt. This allows the existence of
|
||||
individual PRs to remain secret while the hash is known to tools that have
|
||||
read access to the PRs
|
||||
|
||||
Encoding data is input into the sha256 hash as follows:
|
||||
|
||||
* Salt string, min 100 bytes.
|
||||
* Organization name of the PR
|
||||
* Repository name of the PR
|
||||
* PR number (decimal string)
|
||||
|
||||
Encoded PR is then passed to process as mime64 encoded without trailing =.
|
||||
|
||||
113
pr-status-service/main.go
Normal file
113
pr-status-service/main.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
* This file is part of Autogits.
|
||||
*
|
||||
* Copyright © 2024 SUSE LLC
|
||||
*
|
||||
* Autogits is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation, either version 2 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* Autogits is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* Foobar. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"src.opensuse.org/autogits/common"
|
||||
)
|
||||
|
||||
const (
|
||||
AppName = "obs-status-service"
|
||||
)
|
||||
|
||||
var obs *common.ObsClient
|
||||
var debug bool
|
||||
var salt string
|
||||
var RabbitMQHost, Topic, orgs *string
|
||||
|
||||
func LogDebug(v ...any) {
|
||||
if debug {
|
||||
log.Println(v...)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
cert := flag.String("cert-file", "", "TLS certificates file")
|
||||
key := flag.String("key-file", "", "Private key for the TLS certificate")
|
||||
listen := flag.String("listen", "[::1]:8080", "Listening string")
|
||||
disableTls := flag.Bool("no-tls", false, "Disable TLS")
|
||||
obsHost := flag.String("obs-host", "https://api.opensuse.org", "OBS API endpoint for package status information")
|
||||
flag.BoolVar(&debug, "debug", false, "Enable debug logging")
|
||||
RabbitMQHost = flag.String("rabbit-mq", "amqps://rabbit.opensuse.org", "RabbitMQ message bus server")
|
||||
Topic = flag.String("topic", "opensuse.obs", "RabbitMQ topic prefix")
|
||||
orgs = flag.String("orgs", "opensuse", "Comma separated list of orgs to watch")
|
||||
flag.Parse()
|
||||
|
||||
salt = os.Getenv("PR_STATUS_SALT")
|
||||
if len(salt) < 100 {
|
||||
log.Fatal("PR_STATUS_SALT must be at least 100 bytes")
|
||||
}
|
||||
|
||||
common.PanicOnError(common.RequireObsSecretToken())
|
||||
|
||||
var err error
|
||||
if obs, err = common.NewObsClient(*obsHost); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
http.HandleFunc("GET /{PR_HASH}", func(res http.ResponseWriter, req *http.Request) {
|
||||
hash := req.PathValue("PR_HASH")
|
||||
isJSON := strings.HasSuffix(hash, ".json") || req.Header.Get("Accept") == "application/json"
|
||||
hash = strings.TrimSuffix(hash, ".json")
|
||||
hash = strings.TrimSuffix(hash, ".svg")
|
||||
|
||||
status := GetPRStatus(hash)
|
||||
if status == nil {
|
||||
res.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if isJSON {
|
||||
res.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(res).Encode(status)
|
||||
return
|
||||
}
|
||||
|
||||
// default SVG
|
||||
res.Header().Set("Content-Type", "image/svg+xml")
|
||||
res.Write([]byte(status.ToSVG()))
|
||||
})
|
||||
|
||||
http.HandleFunc("POST /{PR_HASH}", func(res http.ResponseWriter, req *http.Request) {
|
||||
hash := req.PathValue("PR_HASH")
|
||||
|
||||
var status PRStatus
|
||||
if err := json.NewDecoder(req.Body).Decode(&status); err != nil {
|
||||
res.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
UpdatePRStatus(hash, &status)
|
||||
res.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
|
||||
go ProcessUpdates()
|
||||
|
||||
if *disableTls {
|
||||
log.Fatal(http.ListenAndServe(*listen, nil))
|
||||
} else {
|
||||
log.Fatal(http.ListenAndServeTLS(*listen, *cert, *key, nil))
|
||||
}
|
||||
}
|
||||
110
pr-status-service/rabbit.go
Normal file
110
pr-status-service/rabbit.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"src.opensuse.org/autogits/common"
|
||||
)
|
||||
|
||||
type PRHandler struct{}
|
||||
|
||||
func (h *PRHandler) ProcessFunc(request *common.Request) error {
|
||||
event, ok := request.Data.(*common.PullRequestWebhookEvent)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
org := event.Repository.Owner.Username
|
||||
repo := event.Repository.Name
|
||||
prNum := fmt.Sprint(event.Number)
|
||||
hash := CalculatePRHash(salt, org, repo, prNum)
|
||||
|
||||
status := &PRStatus{
|
||||
PR: fmt.Sprintf("%s/%s#%s", org, repo, prNum),
|
||||
IsReviewed: false,
|
||||
IsMergeable: event.Pull_Request.State == "open",
|
||||
MergeStatus: event.Pull_Request.State,
|
||||
}
|
||||
|
||||
UpdatePRStatus(hash, status)
|
||||
return nil
|
||||
}
|
||||
|
||||
type ReviewHandler struct {
|
||||
Approved bool
|
||||
}
|
||||
|
||||
func (h *ReviewHandler) ProcessFunc(request *common.Request) error {
|
||||
event, ok := request.Data.(*common.PullRequestWebhookEvent)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
org := event.Repository.Owner.Username
|
||||
repo := event.Repository.Name
|
||||
prNum := fmt.Sprint(event.Number)
|
||||
hash := CalculatePRHash(salt, org, repo, prNum)
|
||||
|
||||
status := GetPRStatus(hash)
|
||||
if status == nil {
|
||||
status = &PRStatus{
|
||||
PR: fmt.Sprintf("%s/%s#%s", org, repo, prNum),
|
||||
}
|
||||
}
|
||||
|
||||
status.IsReviewed = true
|
||||
isApproved := IsApproved_No
|
||||
if h.Approved {
|
||||
isApproved = IsApproved_Yes
|
||||
status.MergeStatus = "approved"
|
||||
} else {
|
||||
status.MergeStatus = "rejected"
|
||||
}
|
||||
|
||||
found := false
|
||||
for i, r := range status.Reviews {
|
||||
if r.Reviewer == event.Sender.Username {
|
||||
status.Reviews[i].IsApproved = isApproved
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
status.Reviews = append(status.Reviews, ReviewStatus{
|
||||
Reviewer: event.Sender.Username,
|
||||
IsApproved: isApproved,
|
||||
})
|
||||
}
|
||||
|
||||
UpdatePRStatus(hash, status)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProcessUpdates() {
|
||||
if RabbitMQHost == nil || *RabbitMQHost == "" {
|
||||
return
|
||||
}
|
||||
|
||||
u, err := url.Parse(*RabbitMQHost)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
processor := &common.RabbitMQGiteaEventsProcessor{
|
||||
Handlers: map[string]common.RequestProcessor{
|
||||
common.RequestType_PR: &PRHandler{},
|
||||
common.RequestType_PRReviewAccepted: &ReviewHandler{Approved: true},
|
||||
common.RequestType_PRReviewRejected: &ReviewHandler{Approved: false},
|
||||
},
|
||||
Orgs: strings.Split(*orgs, ","),
|
||||
}
|
||||
processor.Connection().RabbitURL = u
|
||||
|
||||
err = common.ProcessRabbitMQEvents(processor)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
91
pr-status-service/status.go
Normal file
91
pr-status-service/status.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
IsApproved_Pending = 0
|
||||
IsApproved_Yes = 1
|
||||
IsApproved_No = 2
|
||||
)
|
||||
|
||||
type ReviewStatus struct {
|
||||
Reviewer string
|
||||
IsApproved int
|
||||
}
|
||||
|
||||
type PRStatus struct {
|
||||
PR string
|
||||
IsReviewed bool
|
||||
IsMergeable bool
|
||||
|
||||
MergeStatus string
|
||||
Reviews []ReviewStatus
|
||||
}
|
||||
|
||||
func (status *PRStatus) ToSVG() string {
|
||||
mergeableText := "NO"
|
||||
mergeableColor := "red"
|
||||
if status.IsMergeable {
|
||||
mergeableText = "YES"
|
||||
mergeableColor = "green"
|
||||
}
|
||||
|
||||
reviewedText := "NO"
|
||||
reviewedColor := "red"
|
||||
if status.IsReviewed {
|
||||
reviewedText = "YES"
|
||||
reviewedColor = "green"
|
||||
}
|
||||
|
||||
var reviewsBuilder strings.Builder
|
||||
for i, r := range status.Reviews {
|
||||
color := "orange"
|
||||
if r.IsApproved == IsApproved_Yes {
|
||||
color = "green"
|
||||
} else if r.IsApproved == IsApproved_No {
|
||||
color = "red"
|
||||
}
|
||||
if i > 0 {
|
||||
reviewsBuilder.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(&reviewsBuilder, `<tspan fill="%s">%s</tspan>`, color, r.Reviewer)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" width="400" height="100">
|
||||
<rect width="400" height="100" fill="#f8f9fa" stroke="#dee2e6"/>
|
||||
<text x="10" y="25" font-family="sans-serif" font-size="14" fill="#333">Is Mergeable: <tspan fill="%s" font-weight="bold">%s</tspan></text>
|
||||
<text x="10" y="45" font-family="sans-serif" font-size="14" fill="#333">Is Reviewed?: <tspan fill="%s" font-weight="bold">%s</tspan></text>
|
||||
<text x="10" y="65" font-family="sans-serif" font-size="14" fill="#333">Merge Status: %s</text>
|
||||
<text x="10" y="85" font-family="sans-serif" font-size="14" fill="#333">Reviews: %s</text>
|
||||
</svg>`, mergeableColor, mergeableText, reviewedColor, reviewedText, status.MergeStatus, reviewsBuilder.String())
|
||||
}
|
||||
|
||||
var prStatuses = make(map[string]*PRStatus)
|
||||
var prStatusesLock sync.RWMutex
|
||||
|
||||
func GetPRStatus(hash string) *PRStatus {
|
||||
prStatusesLock.RLock()
|
||||
defer prStatusesLock.RUnlock()
|
||||
return prStatuses[hash]
|
||||
}
|
||||
|
||||
func UpdatePRStatus(hash string, status *PRStatus) {
|
||||
prStatusesLock.Lock()
|
||||
defer prStatusesLock.Unlock()
|
||||
prStatuses[hash] = status
|
||||
}
|
||||
|
||||
func CalculatePRHash(salt, org, repo, prNum string) string {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(salt))
|
||||
h.Write([]byte(org))
|
||||
h.Write([]byte(repo))
|
||||
h.Write([]byte(prNum))
|
||||
return base64.RawStdEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
185
pr-status-service/status_test.go
Normal file
185
pr-status-service/status_test.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"src.opensuse.org/autogits/common"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCalculatePRHash(t *testing.T) {
|
||||
salt = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" // 100 bytes
|
||||
org := "opensuse"
|
||||
repo := "pr-status-service"
|
||||
prNum := "1"
|
||||
|
||||
hash := CalculatePRHash(salt, org, repo, prNum)
|
||||
if hash == "" {
|
||||
t.Errorf("Hash is empty")
|
||||
}
|
||||
|
||||
hash2 := CalculatePRHash(salt, org, repo, prNum)
|
||||
if hash != hash2 {
|
||||
t.Errorf("Hash is not consistent")
|
||||
}
|
||||
|
||||
// Different inputs should give different hashes
|
||||
hash3 := CalculatePRHash(salt, org, repo, "2")
|
||||
if hash == hash3 {
|
||||
t.Errorf("Hashes for different PR numbers are same")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRStatusToSVG(t *testing.T) {
|
||||
status := &PRStatus{
|
||||
PR: "org/repo#1",
|
||||
IsMergeable: true,
|
||||
IsReviewed: true,
|
||||
MergeStatus: "can be merged",
|
||||
Reviews: []ReviewStatus{
|
||||
{Reviewer: "alice", IsApproved: IsApproved_Yes},
|
||||
{Reviewer: "bob", IsApproved: IsApproved_No},
|
||||
{Reviewer: "charlie", IsApproved: IsApproved_Pending},
|
||||
},
|
||||
}
|
||||
|
||||
svg := status.ToSVG()
|
||||
|
||||
// Check for key elements in the SVG
|
||||
expectedStrings := []string{
|
||||
`Is Mergeable: <tspan fill="green" font-weight="bold">YES</tspan>`,
|
||||
`Is Reviewed?: <tspan fill="green" font-weight="bold">YES</tspan>`,
|
||||
`Merge Status: can be merged`,
|
||||
`<tspan fill="green">alice</tspan>`,
|
||||
`<tspan fill="red">bob</tspan>`,
|
||||
`<tspan fill="orange">charlie</tspan>`,
|
||||
}
|
||||
|
||||
for _, expected := range expectedStrings {
|
||||
if !contains(svg, expected) {
|
||||
t.Errorf("SVG missing expected string: %s", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRStatusStore(t *testing.T) {
|
||||
hash := "test-hash"
|
||||
status := &PRStatus{PR: "org/repo#123"}
|
||||
|
||||
UpdatePRStatus(hash, status)
|
||||
|
||||
ret := GetPRStatus(hash)
|
||||
if ret == nil || ret.PR != status.PR {
|
||||
t.Errorf("Retrieved status does not match stored status")
|
||||
}
|
||||
|
||||
retNil := GetPRStatus("non-existent")
|
||||
if retNil != nil {
|
||||
t.Errorf("Expected nil for non-existent hash")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlers(t *testing.T) {
|
||||
salt = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
|
||||
|
||||
repo := &common.Repository{
|
||||
Name: "test-repo",
|
||||
Owner: &common.Organization{
|
||||
Username: "test-org",
|
||||
},
|
||||
}
|
||||
|
||||
prHandler := &PRHandler{}
|
||||
prEvent := &common.PullRequestWebhookEvent{
|
||||
Number: 1,
|
||||
Repository: repo,
|
||||
Pull_Request: &common.PullRequest{
|
||||
State: "open",
|
||||
},
|
||||
}
|
||||
|
||||
err := prHandler.ProcessFunc(&common.Request{Data: prEvent})
|
||||
if err != nil {
|
||||
t.Errorf("PRHandler error: %v", err)
|
||||
}
|
||||
|
||||
hash := CalculatePRHash(salt, "test-org", "test-repo", "1")
|
||||
status := GetPRStatus(hash)
|
||||
if status == nil {
|
||||
t.Fatalf("Status not found after PRHandler")
|
||||
}
|
||||
|
||||
if status.IsMergeable != true {
|
||||
t.Errorf("Expected mergeable true")
|
||||
}
|
||||
|
||||
// Test ReviewHandler
|
||||
reviewHandler := &ReviewHandler{Approved: true}
|
||||
reviewEvent := &common.PullRequestWebhookEvent{
|
||||
Number: 1,
|
||||
Repository: repo,
|
||||
Sender: common.User{
|
||||
Username: "reviewer1",
|
||||
},
|
||||
}
|
||||
|
||||
err = reviewHandler.ProcessFunc(&common.Request{Data: reviewEvent})
|
||||
if err != nil {
|
||||
t.Errorf("ReviewHandler error: %v", err)
|
||||
}
|
||||
|
||||
status = GetPRStatus(hash)
|
||||
if !status.IsReviewed {
|
||||
t.Errorf("Expected IsReviewed true")
|
||||
}
|
||||
if len(status.Reviews) != 1 || status.Reviews[0].Reviewer != "reviewer1" || status.Reviews[0].IsApproved != IsApproved_Yes {
|
||||
t.Errorf("Review not correctly recorded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReviewUpdate(t *testing.T) {
|
||||
repo := &common.Repository{
|
||||
Name: "test-repo",
|
||||
Owner: &common.Organization{
|
||||
Username: "test-org",
|
||||
},
|
||||
}
|
||||
hash := CalculatePRHash(salt, "test-org", "test-repo", "1")
|
||||
UpdatePRStatus(hash, &PRStatus{PR: "test-org/test-repo#1"})
|
||||
|
||||
reviewHandler := &ReviewHandler{Approved: true}
|
||||
reviewEvent := &common.PullRequestWebhookEvent{
|
||||
Number: 1,
|
||||
Repository: repo,
|
||||
Sender: common.User{
|
||||
Username: "reviewer1",
|
||||
},
|
||||
}
|
||||
|
||||
reviewHandler.ProcessFunc(&common.Request{Data: reviewEvent})
|
||||
|
||||
// Update same review
|
||||
reviewHandler.Approved = false
|
||||
reviewHandler.ProcessFunc(&common.Request{Data: reviewEvent})
|
||||
|
||||
status := GetPRStatus(hash)
|
||||
if len(status.Reviews) != 1 || status.Reviews[0].IsApproved != IsApproved_No {
|
||||
t.Errorf("Review not correctly updated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerErrors(t *testing.T) {
|
||||
prHandler := &PRHandler{}
|
||||
err := prHandler.ProcessFunc(&common.Request{Data: nil})
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error for nil data in PRHandler")
|
||||
}
|
||||
|
||||
reviewHandler := &ReviewHandler{}
|
||||
err = reviewHandler.ProcessFunc(&common.Request{Data: nil})
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error for nil data in ReviewHandler")
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || (len(substr) > 0 && (s[:len(substr)] == substr || contains(s[1:], substr))))
|
||||
}
|
||||
@@ -96,19 +96,6 @@ Package Deletion Requests
|
||||
If you wish to re-add a package, create a new PrjGit PR which adds again the submodule on the branch that has the "-removed" suffix. The bot will automatically remove this suffix from the project branch in the pool.
|
||||
|
||||
|
||||
Merge Modes
|
||||
-----------
|
||||
|
||||
| Merge Mode | Description
|
||||
|------------|--------------------------------------------------------------------------------
|
||||
| ff-only | Only allow --ff-only merges in the package branch. This is best suited for
|
||||
| | devel projects and openSUSE Tumbleweed development, where history should be linear
|
||||
| replace | Merge is done via `-X theirs` strategy and old files are removed in the merge.
|
||||
| | This works well for downstream codestreams, like Leap, that would update their branch
|
||||
| | using latest version.
|
||||
| devel | No merge, just set the project branch to PR HEAD. This is suitable for downstream
|
||||
| | projects like Leap during development cycle, where keeping maintenance history is not important
|
||||
|
||||
Labels
|
||||
------
|
||||
|
||||
|
||||
@@ -417,12 +417,6 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
|
||||
}
|
||||
common.LogInfo("fetched PRSet of size:", len(prset.PRs))
|
||||
|
||||
if !prset.PrepareForMerge(git) {
|
||||
common.LogError("PRs are NOT mergeable.")
|
||||
} else {
|
||||
common.LogInfo("PRs are in mergeable state.")
|
||||
}
|
||||
|
||||
prjGitPRbranch := prGitBranchNameForPR(prRepo, prNo)
|
||||
prjGitPR, err := prset.GetPrjGitPR()
|
||||
if err == common.PRSet_PrjGitMissing {
|
||||
|
||||
Reference in New Issue
Block a user