Compare commits
24 Commits
status
...
improve_po
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
527182ff96
|
||
|
|
e1ed2e78e0 | ||
|
|
e494d545e7 | ||
|
|
e1ce27250b | ||
|
|
f87b3345fc | ||
|
|
5440476d10 | ||
| ba20810c99 | |||
|
|
f5a32792e0 | ||
| b4945a8ae4 | |||
| 1451266ddc | |||
| bd618983e9 | |||
| c7840ddd47 | |||
| 3f110ce5f6 | |||
| 3a2c87b4af | |||
| d0056ed461 | |||
| e5e1b5d9a5 | |||
|
|
96a908d0be | ||
|
|
6aaff89179 | ||
| 7ec663db27 | |||
| 3b83ba96e4 | |||
| 3a0445e857 | |||
| ef5db8ca28 | |||
| 10f74f681d | |||
| b514f9784c |
@@ -34,19 +34,27 @@ jobs:
|
|||||||
run: make build
|
run: make build
|
||||||
working-directory: ./autogits
|
working-directory: ./autogits
|
||||||
- name: Prepare images
|
- name: Prepare images
|
||||||
run: make build
|
run: |
|
||||||
|
make build
|
||||||
|
podman rmi $(podman images -f "dangling=true" -q)
|
||||||
working-directory: ./autogits/integration
|
working-directory: ./autogits/integration
|
||||||
- name: Make sure the pod is down
|
- name: Make sure the pod is down
|
||||||
run: make down
|
run: make down
|
||||||
working-directory: ./autogits/integration
|
working-directory: ./autogits/integration
|
||||||
- name: Start images
|
- name: Start images
|
||||||
run: make up
|
run: |
|
||||||
|
make up
|
||||||
|
make wait_healthy
|
||||||
|
podman ps
|
||||||
|
sleep 5
|
||||||
working-directory: ./autogits/integration
|
working-directory: ./autogits/integration
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: py.test-3.11 -v tests
|
run: make pytest
|
||||||
working-directory: ./autogits/integration
|
working-directory: ./autogits/integration
|
||||||
- name: Make sure the pod is down
|
- name: Make sure the pod is down
|
||||||
if: always()
|
if: always()
|
||||||
run: make down
|
run: |
|
||||||
|
podman ps
|
||||||
|
make down
|
||||||
working-directory: ./autogits/integration
|
working-directory: ./autogits/integration
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ const (
|
|||||||
|
|
||||||
Permission_ForceMerge = "force-merge"
|
Permission_ForceMerge = "force-merge"
|
||||||
Permission_Group = "release-engineering"
|
Permission_Group = "release-engineering"
|
||||||
|
|
||||||
|
MergeModeFF = "ff-only"
|
||||||
|
MergeModeReplace = "replace"
|
||||||
|
MergeModeDevel = "devel"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigFile struct {
|
type ConfigFile struct {
|
||||||
@@ -52,9 +56,9 @@ type ReviewGroup struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type QAConfig struct {
|
type QAConfig struct {
|
||||||
Name string
|
Name string
|
||||||
Origin string
|
Origin string
|
||||||
Label string // requires this gitea lable to be set or skipped
|
Label string // requires this gitea lable to be set or skipped
|
||||||
BuildDisableRepos []string // which repos to build disable in the new project
|
BuildDisableRepos []string // which repos to build disable in the new project
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +93,8 @@ type AutogitConfig struct {
|
|||||||
Committers []string // group in addition to Reviewers and Maintainers that can order the bot around, mostly as helper for factory-maintainers
|
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
|
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
|
Labels map[string]string // list of tags, if not default, to apply
|
||||||
|
MergeMode string // project merge mode
|
||||||
|
|
||||||
NoProjectGitPR bool // do not automatically create project git PRs, just assign reviewers and assume somethign else creates the ProjectGit PR
|
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
|
ManualMergeOnly bool // only merge with "Merge OK" comment by Project Maintainers and/or Package Maintainers and/or reviewers
|
||||||
@@ -184,6 +189,17 @@ func ReadWorkflowConfig(gitea GiteaFileContentAndRepoFetcher, git_project string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
config.GitProjectName = config.GitProjectName + "#" + branch
|
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
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"go.uber.org/mock/gomock"
|
||||||
"src.opensuse.org/autogits/common"
|
"src.opensuse.org/autogits/common"
|
||||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||||
mock_common "src.opensuse.org/autogits/common/mock"
|
mock_common "src.opensuse.org/autogits/common/mock"
|
||||||
@@ -341,3 +342,67 @@ 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -396,12 +396,17 @@ func (e *GitHandlerImpl) GitExecQuietOrPanic(cwd string, params ...string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ChanIO struct {
|
type ChanIO struct {
|
||||||
ch chan byte
|
ch chan byte
|
||||||
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ChanIO) Write(p []byte) (int, error) {
|
func (c *ChanIO) Write(p []byte) (int, error) {
|
||||||
for _, b := range p {
|
for _, b := range p {
|
||||||
c.ch <- b
|
select {
|
||||||
|
case c.ch <- b:
|
||||||
|
case <-c.done:
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return len(p), nil
|
return len(p), nil
|
||||||
}
|
}
|
||||||
@@ -410,21 +415,32 @@ func (c *ChanIO) Write(p []byte) (int, error) {
|
|||||||
func (c *ChanIO) Read(data []byte) (idx int, err error) {
|
func (c *ChanIO) Read(data []byte) (idx int, err error) {
|
||||||
var ok bool
|
var ok bool
|
||||||
|
|
||||||
data[idx], ok = <-c.ch
|
select {
|
||||||
if !ok {
|
case data[idx], ok = <-c.ch:
|
||||||
err = io.EOF
|
|
||||||
return
|
|
||||||
}
|
|
||||||
idx++
|
|
||||||
|
|
||||||
for len(c.ch) > 0 && idx < len(data) {
|
|
||||||
data[idx], ok = <-c.ch
|
|
||||||
if !ok {
|
if !ok {
|
||||||
err = io.EOF
|
err = io.EOF
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
idx++
|
idx++
|
||||||
|
case <-c.done:
|
||||||
|
err = io.EOF
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for len(c.ch) > 0 && idx < len(data) {
|
||||||
|
select {
|
||||||
|
case data[idx], ok = <-c.ch:
|
||||||
|
if !ok {
|
||||||
|
err = io.EOF
|
||||||
|
return
|
||||||
|
}
|
||||||
|
idx++
|
||||||
|
case <-c.done:
|
||||||
|
err = io.EOF
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -471,7 +487,14 @@ func parseGitMsg(data <-chan byte) (GitMsg, error) {
|
|||||||
var size int
|
var size int
|
||||||
|
|
||||||
pos := 0
|
pos := 0
|
||||||
for c := <-data; c != ' '; c = <-data {
|
for {
|
||||||
|
c, ok := <-data
|
||||||
|
if !ok {
|
||||||
|
return GitMsg{}, io.EOF
|
||||||
|
}
|
||||||
|
if c == ' ' {
|
||||||
|
break
|
||||||
|
}
|
||||||
if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') {
|
if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') {
|
||||||
id[pos] = c
|
id[pos] = c
|
||||||
pos++
|
pos++
|
||||||
@@ -483,7 +506,15 @@ func parseGitMsg(data <-chan byte) (GitMsg, error) {
|
|||||||
|
|
||||||
pos = 0
|
pos = 0
|
||||||
var c byte
|
var c byte
|
||||||
for c = <-data; c != ' ' && c != '\x00'; c = <-data {
|
for {
|
||||||
|
var ok bool
|
||||||
|
c, ok = <-data
|
||||||
|
if !ok {
|
||||||
|
return GitMsg{}, io.EOF
|
||||||
|
}
|
||||||
|
if c == ' ' || c == '\x00' {
|
||||||
|
break
|
||||||
|
}
|
||||||
if c >= 'a' && c <= 'z' {
|
if c >= 'a' && c <= 'z' {
|
||||||
msgType[pos] = c
|
msgType[pos] = c
|
||||||
pos++
|
pos++
|
||||||
@@ -509,7 +540,14 @@ func parseGitMsg(data <-chan byte) (GitMsg, error) {
|
|||||||
return GitMsg{}, fmt.Errorf("Invalid object type: '%s'", string(msgType))
|
return GitMsg{}, fmt.Errorf("Invalid object type: '%s'", string(msgType))
|
||||||
}
|
}
|
||||||
|
|
||||||
for c = <-data; c != '\000'; c = <-data {
|
for {
|
||||||
|
c, ok := <-data
|
||||||
|
if !ok {
|
||||||
|
return GitMsg{}, io.EOF
|
||||||
|
}
|
||||||
|
if c == '\x00' {
|
||||||
|
break
|
||||||
|
}
|
||||||
if c >= '0' && c <= '9' {
|
if c >= '0' && c <= '9' {
|
||||||
size = size*10 + (int(c) - '0')
|
size = size*10 + (int(c) - '0')
|
||||||
} else {
|
} else {
|
||||||
@@ -528,18 +566,37 @@ func parseGitCommitHdr(oldHdr [2]string, data <-chan byte) ([2]string, int, erro
|
|||||||
hdr := make([]byte, 0, 60)
|
hdr := make([]byte, 0, 60)
|
||||||
val := make([]byte, 0, 1000)
|
val := make([]byte, 0, 1000)
|
||||||
|
|
||||||
c := <-data
|
c, ok := <-data
|
||||||
|
if !ok {
|
||||||
|
return [2]string{}, 0, io.EOF
|
||||||
|
}
|
||||||
size := 1
|
size := 1
|
||||||
if c != '\n' { // end of header marker
|
if c != '\n' { // end of header marker
|
||||||
for ; c != ' '; c = <-data {
|
for {
|
||||||
|
if c == ' ' {
|
||||||
|
break
|
||||||
|
}
|
||||||
hdr = append(hdr, c)
|
hdr = append(hdr, c)
|
||||||
size++
|
size++
|
||||||
|
var ok bool
|
||||||
|
c, ok = <-data
|
||||||
|
if !ok {
|
||||||
|
return [2]string{}, size, io.EOF
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if size == 1 { // continuation header here
|
if size == 1 { // continuation header here
|
||||||
hdr = []byte(oldHdr[0])
|
hdr = []byte(oldHdr[0])
|
||||||
val = append([]byte(oldHdr[1]), '\n')
|
val = append([]byte(oldHdr[1]), '\n')
|
||||||
}
|
}
|
||||||
for c := <-data; c != '\n'; c = <-data {
|
for {
|
||||||
|
var ok bool
|
||||||
|
c, ok = <-data
|
||||||
|
if !ok {
|
||||||
|
return [2]string{}, size, io.EOF
|
||||||
|
}
|
||||||
|
if c == '\n' {
|
||||||
|
break
|
||||||
|
}
|
||||||
val = append(val, c)
|
val = append(val, c)
|
||||||
size++
|
size++
|
||||||
}
|
}
|
||||||
@@ -552,7 +609,14 @@ func parseGitCommitHdr(oldHdr [2]string, data <-chan byte) ([2]string, int, erro
|
|||||||
func parseGitCommitMsg(data <-chan byte, l int) (string, error) {
|
func parseGitCommitMsg(data <-chan byte, l int) (string, error) {
|
||||||
msg := make([]byte, 0, l)
|
msg := make([]byte, 0, l)
|
||||||
|
|
||||||
for c := <-data; c != '\x00'; c = <-data {
|
for {
|
||||||
|
c, ok := <-data
|
||||||
|
if !ok {
|
||||||
|
return string(msg), io.EOF
|
||||||
|
}
|
||||||
|
if c == '\x00' {
|
||||||
|
break
|
||||||
|
}
|
||||||
msg = append(msg, c)
|
msg = append(msg, c)
|
||||||
l--
|
l--
|
||||||
}
|
}
|
||||||
@@ -578,7 +642,7 @@ func parseGitCommit(data <-chan byte) (GitCommit, error) {
|
|||||||
var hdr [2]string
|
var hdr [2]string
|
||||||
hdr, size, err := parseGitCommitHdr(hdr, data)
|
hdr, size, err := parseGitCommitHdr(hdr, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return GitCommit{}, nil
|
return GitCommit{}, err
|
||||||
}
|
}
|
||||||
l -= size
|
l -= size
|
||||||
|
|
||||||
@@ -599,14 +663,28 @@ func parseGitCommit(data <-chan byte) (GitCommit, error) {
|
|||||||
func parseTreeEntry(data <-chan byte, hashLen int) (GitTreeEntry, error) {
|
func parseTreeEntry(data <-chan byte, hashLen int) (GitTreeEntry, error) {
|
||||||
var e GitTreeEntry
|
var e GitTreeEntry
|
||||||
|
|
||||||
for c := <-data; c != ' '; c = <-data {
|
for {
|
||||||
|
c, ok := <-data
|
||||||
|
if !ok {
|
||||||
|
return e, io.EOF
|
||||||
|
}
|
||||||
|
if c == ' ' {
|
||||||
|
break
|
||||||
|
}
|
||||||
e.mode = e.mode*8 + int(c-'0')
|
e.mode = e.mode*8 + int(c-'0')
|
||||||
e.size++
|
e.size++
|
||||||
}
|
}
|
||||||
e.size++
|
e.size++
|
||||||
|
|
||||||
name := make([]byte, 0, 128)
|
name := make([]byte, 0, 128)
|
||||||
for c := <-data; c != '\x00'; c = <-data {
|
for {
|
||||||
|
c, ok := <-data
|
||||||
|
if !ok {
|
||||||
|
return e, io.EOF
|
||||||
|
}
|
||||||
|
if c == '\x00' {
|
||||||
|
break
|
||||||
|
}
|
||||||
name = append(name, c)
|
name = append(name, c)
|
||||||
e.size++
|
e.size++
|
||||||
}
|
}
|
||||||
@@ -617,7 +695,10 @@ func parseTreeEntry(data <-chan byte, hashLen int) (GitTreeEntry, error) {
|
|||||||
|
|
||||||
hash := make([]byte, 0, hashLen*2)
|
hash := make([]byte, 0, hashLen*2)
|
||||||
for range hashLen {
|
for range hashLen {
|
||||||
c := <-data
|
c, ok := <-data
|
||||||
|
if !ok {
|
||||||
|
return e, io.EOF
|
||||||
|
}
|
||||||
hash = append(hash, hexBinToAscii[((c&0xF0)>>4)], hexBinToAscii[c&0xF])
|
hash = append(hash, hexBinToAscii[((c&0xF0)>>4)], hexBinToAscii[c&0xF])
|
||||||
}
|
}
|
||||||
e.hash = string(hash)
|
e.hash = string(hash)
|
||||||
@@ -638,13 +719,16 @@ func parseGitTree(data <-chan byte) (GitTree, error) {
|
|||||||
for parsedLen < hdr.size {
|
for parsedLen < hdr.size {
|
||||||
entry, err := parseTreeEntry(data, len(hdr.hash)/2)
|
entry, err := parseTreeEntry(data, len(hdr.hash)/2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return GitTree{}, nil
|
return GitTree{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
t.items = append(t.items, entry)
|
t.items = append(t.items, entry)
|
||||||
parsedLen += entry.size
|
parsedLen += entry.size
|
||||||
}
|
}
|
||||||
c := <-data // \0 read
|
c, ok := <-data // \0 read
|
||||||
|
if !ok {
|
||||||
|
return t, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
if c != '\x00' {
|
if c != '\x00' {
|
||||||
return t, fmt.Errorf("Unexpected character during git tree data read")
|
return t, fmt.Errorf("Unexpected character during git tree data read")
|
||||||
@@ -665,9 +749,16 @@ func parseGitBlob(data <-chan byte) ([]byte, error) {
|
|||||||
|
|
||||||
d := make([]byte, hdr.size)
|
d := make([]byte, hdr.size)
|
||||||
for l := 0; l < hdr.size; l++ {
|
for l := 0; l < hdr.size; l++ {
|
||||||
d[l] = <-data
|
var ok bool
|
||||||
|
d[l], ok = <-data
|
||||||
|
if !ok {
|
||||||
|
return d, io.EOF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eob, ok := <-data
|
||||||
|
if !ok {
|
||||||
|
return d, io.EOF
|
||||||
}
|
}
|
||||||
eob := <-data
|
|
||||||
if eob != '\x00' {
|
if eob != '\x00' {
|
||||||
return d, fmt.Errorf("invalid byte read in parseGitBlob")
|
return d, fmt.Errorf("invalid byte read in parseGitBlob")
|
||||||
}
|
}
|
||||||
@@ -679,16 +770,25 @@ func (e *GitHandlerImpl) GitParseCommits(cwd string, commitIDs []string) (parsed
|
|||||||
var done sync.Mutex
|
var done sync.Mutex
|
||||||
|
|
||||||
done.Lock()
|
done.Lock()
|
||||||
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
done_signal := make(chan struct{})
|
||||||
|
var once sync.Once
|
||||||
|
close_done := func() {
|
||||||
|
once.Do(func() {
|
||||||
|
close(done_signal)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
data_in, data_out := ChanIO{make(chan byte), done_signal}, ChanIO{make(chan byte), done_signal}
|
||||||
parsedCommits = make([]GitCommit, 0, len(commitIDs))
|
parsedCommits = make([]GitCommit, 0, len(commitIDs))
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer done.Unlock()
|
defer done.Unlock()
|
||||||
|
defer close_done()
|
||||||
defer close(data_out.ch)
|
defer close(data_out.ch)
|
||||||
|
|
||||||
for _, id := range commitIDs {
|
for _, id := range commitIDs {
|
||||||
data_out.Write([]byte(id))
|
data_out.Write([]byte(id))
|
||||||
data_out.ch <- '\x00'
|
data_out.Write([]byte{0})
|
||||||
c, e := parseGitCommit(data_in.ch)
|
c, e := parseGitCommit(data_in.ch)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
err = fmt.Errorf("Error parsing git commit: %w", e)
|
err = fmt.Errorf("Error parsing git commit: %w", e)
|
||||||
@@ -715,12 +815,14 @@ func (e *GitHandlerImpl) GitParseCommits(cwd string, commitIDs []string) (parsed
|
|||||||
LogDebug("command run:", cmd.Args)
|
LogDebug("command run:", cmd.Args)
|
||||||
if e := cmd.Run(); e != nil {
|
if e := cmd.Run(); e != nil {
|
||||||
LogError(e)
|
LogError(e)
|
||||||
|
close_done()
|
||||||
close(data_in.ch)
|
close(data_in.ch)
|
||||||
close(data_out.ch)
|
|
||||||
return nil, e
|
return nil, e
|
||||||
}
|
}
|
||||||
|
|
||||||
done.Lock()
|
done.Lock()
|
||||||
|
close_done()
|
||||||
|
close(data_in.ch)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -729,15 +831,21 @@ func (e *GitHandlerImpl) GitCatFile(cwd, commitId, filename string) (data []byte
|
|||||||
var done sync.Mutex
|
var done sync.Mutex
|
||||||
|
|
||||||
done.Lock()
|
done.Lock()
|
||||||
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
done_signal := make(chan struct{})
|
||||||
|
var once sync.Once
|
||||||
|
close_done := func() {
|
||||||
|
once.Do(func() {
|
||||||
|
close(done_signal)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
data_in, data_out := ChanIO{make(chan byte), done_signal}, ChanIO{make(chan byte), done_signal}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer done.Unlock()
|
defer done.Unlock()
|
||||||
|
defer close_done()
|
||||||
defer close(data_out.ch)
|
defer close(data_out.ch)
|
||||||
|
|
||||||
data_out.Write([]byte(commitId))
|
data_out.Write([]byte(commitId))
|
||||||
data_out.ch <- '\x00'
|
data_out.Write([]byte{0})
|
||||||
|
|
||||||
var c GitCommit
|
var c GitCommit
|
||||||
c, err = parseGitCommit(data_in.ch)
|
c, err = parseGitCommit(data_in.ch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -745,11 +853,9 @@ func (e *GitHandlerImpl) GitCatFile(cwd, commitId, filename string) (data []byte
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
data_out.Write([]byte(c.Tree))
|
data_out.Write([]byte(c.Tree))
|
||||||
data_out.ch <- '\x00'
|
data_out.Write([]byte{0})
|
||||||
|
|
||||||
var tree GitTree
|
var tree GitTree
|
||||||
tree, err = parseGitTree(data_in.ch)
|
tree, err = parseGitTree(data_in.ch)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LogError("Error parsing git tree:", err)
|
LogError("Error parsing git tree:", err)
|
||||||
return
|
return
|
||||||
@@ -759,7 +865,7 @@ func (e *GitHandlerImpl) GitCatFile(cwd, commitId, filename string) (data []byte
|
|||||||
if te.isBlob() && te.name == filename {
|
if te.isBlob() && te.name == filename {
|
||||||
LogInfo("blob", te.hash)
|
LogInfo("blob", te.hash)
|
||||||
data_out.Write([]byte(te.hash))
|
data_out.Write([]byte(te.hash))
|
||||||
data_out.ch <- '\x00'
|
data_out.Write([]byte{0})
|
||||||
data, err = parseGitBlob(data_in.ch)
|
data, err = parseGitBlob(data_in.ch)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -784,11 +890,13 @@ func (e *GitHandlerImpl) GitCatFile(cwd, commitId, filename string) (data []byte
|
|||||||
LogDebug("command run:", cmd.Args)
|
LogDebug("command run:", cmd.Args)
|
||||||
if e := cmd.Run(); e != nil {
|
if e := cmd.Run(); e != nil {
|
||||||
LogError(e)
|
LogError(e)
|
||||||
|
close_done()
|
||||||
close(data_in.ch)
|
close(data_in.ch)
|
||||||
close(data_out.ch)
|
|
||||||
return nil, e
|
return nil, e
|
||||||
}
|
}
|
||||||
done.Lock()
|
done.Lock()
|
||||||
|
close_done()
|
||||||
|
close(data_in.ch)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -798,16 +906,24 @@ func (e *GitHandlerImpl) GitDirectoryList(gitPath, commitId string) (directoryLi
|
|||||||
directoryList = make(map[string]string)
|
directoryList = make(map[string]string)
|
||||||
|
|
||||||
done.Lock()
|
done.Lock()
|
||||||
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
done_signal := make(chan struct{})
|
||||||
|
var once sync.Once
|
||||||
|
close_done := func() {
|
||||||
|
once.Do(func() {
|
||||||
|
close(done_signal)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
data_in, data_out := ChanIO{make(chan byte), done_signal}, ChanIO{make(chan byte), done_signal}
|
||||||
|
|
||||||
LogDebug("Getting directory for:", commitId)
|
LogDebug("Getting directory for:", commitId)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer done.Unlock()
|
defer done.Unlock()
|
||||||
|
defer close_done()
|
||||||
defer close(data_out.ch)
|
defer close(data_out.ch)
|
||||||
|
|
||||||
data_out.Write([]byte(commitId))
|
data_out.Write([]byte(commitId))
|
||||||
data_out.ch <- '\x00'
|
data_out.Write([]byte{0})
|
||||||
var c GitCommit
|
var c GitCommit
|
||||||
c, err = parseGitCommit(data_in.ch)
|
c, err = parseGitCommit(data_in.ch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -823,7 +939,7 @@ func (e *GitHandlerImpl) GitDirectoryList(gitPath, commitId string) (directoryLi
|
|||||||
delete(trees, p)
|
delete(trees, p)
|
||||||
|
|
||||||
data_out.Write([]byte(tree))
|
data_out.Write([]byte(tree))
|
||||||
data_out.ch <- '\x00'
|
data_out.Write([]byte{0})
|
||||||
var tree GitTree
|
var tree GitTree
|
||||||
tree, err = parseGitTree(data_in.ch)
|
tree, err = parseGitTree(data_in.ch)
|
||||||
|
|
||||||
@@ -857,12 +973,14 @@ func (e *GitHandlerImpl) GitDirectoryList(gitPath, commitId string) (directoryLi
|
|||||||
LogDebug("command run:", cmd.Args)
|
LogDebug("command run:", cmd.Args)
|
||||||
if e := cmd.Run(); e != nil {
|
if e := cmd.Run(); e != nil {
|
||||||
LogError(e)
|
LogError(e)
|
||||||
|
close_done()
|
||||||
close(data_in.ch)
|
close(data_in.ch)
|
||||||
close(data_out.ch)
|
|
||||||
return directoryList, e
|
return directoryList, e
|
||||||
}
|
}
|
||||||
|
|
||||||
done.Lock()
|
done.Lock()
|
||||||
|
close_done()
|
||||||
|
close(data_in.ch)
|
||||||
return directoryList, err
|
return directoryList, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -872,7 +990,14 @@ func (e *GitHandlerImpl) GitDirectoryContentList(gitPath, commitId string) (dire
|
|||||||
directoryList = make(map[string]string)
|
directoryList = make(map[string]string)
|
||||||
|
|
||||||
done.Lock()
|
done.Lock()
|
||||||
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
done_signal := make(chan struct{})
|
||||||
|
var once sync.Once
|
||||||
|
close_done := func() {
|
||||||
|
once.Do(func() {
|
||||||
|
close(done_signal)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
data_in, data_out := ChanIO{make(chan byte), done_signal}, ChanIO{make(chan byte), done_signal}
|
||||||
|
|
||||||
LogDebug("Getting directory content for:", commitId)
|
LogDebug("Getting directory content for:", commitId)
|
||||||
|
|
||||||
@@ -881,7 +1006,7 @@ func (e *GitHandlerImpl) GitDirectoryContentList(gitPath, commitId string) (dire
|
|||||||
defer close(data_out.ch)
|
defer close(data_out.ch)
|
||||||
|
|
||||||
data_out.Write([]byte(commitId))
|
data_out.Write([]byte(commitId))
|
||||||
data_out.ch <- '\x00'
|
data_out.Write([]byte{0})
|
||||||
var c GitCommit
|
var c GitCommit
|
||||||
c, err = parseGitCommit(data_in.ch)
|
c, err = parseGitCommit(data_in.ch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -897,7 +1022,7 @@ func (e *GitHandlerImpl) GitDirectoryContentList(gitPath, commitId string) (dire
|
|||||||
delete(trees, p)
|
delete(trees, p)
|
||||||
|
|
||||||
data_out.Write([]byte(tree))
|
data_out.Write([]byte(tree))
|
||||||
data_out.ch <- '\x00'
|
data_out.Write([]byte{0})
|
||||||
var tree GitTree
|
var tree GitTree
|
||||||
tree, err = parseGitTree(data_in.ch)
|
tree, err = parseGitTree(data_in.ch)
|
||||||
|
|
||||||
@@ -933,12 +1058,14 @@ func (e *GitHandlerImpl) GitDirectoryContentList(gitPath, commitId string) (dire
|
|||||||
LogDebug("command run:", cmd.Args)
|
LogDebug("command run:", cmd.Args)
|
||||||
if e := cmd.Run(); e != nil {
|
if e := cmd.Run(); e != nil {
|
||||||
LogError(e)
|
LogError(e)
|
||||||
|
close_done()
|
||||||
close(data_in.ch)
|
close(data_in.ch)
|
||||||
close(data_out.ch)
|
|
||||||
return directoryList, e
|
return directoryList, e
|
||||||
}
|
}
|
||||||
|
|
||||||
done.Lock()
|
done.Lock()
|
||||||
|
close_done()
|
||||||
|
close(data_in.ch)
|
||||||
return directoryList, err
|
return directoryList, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -948,16 +1075,24 @@ func (e *GitHandlerImpl) GitSubmoduleList(gitPath, commitId string) (submoduleLi
|
|||||||
submoduleList = make(map[string]string)
|
submoduleList = make(map[string]string)
|
||||||
|
|
||||||
done.Lock()
|
done.Lock()
|
||||||
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
done_signal := make(chan struct{})
|
||||||
|
var once sync.Once
|
||||||
|
close_done := func() {
|
||||||
|
once.Do(func() {
|
||||||
|
close(done_signal)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
data_in, data_out := ChanIO{make(chan byte), done_signal}, ChanIO{make(chan byte), done_signal}
|
||||||
|
|
||||||
LogDebug("Getting submodules for:", commitId)
|
LogDebug("Getting submodules for:", commitId)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer done.Unlock()
|
defer done.Unlock()
|
||||||
|
defer close_done()
|
||||||
defer close(data_out.ch)
|
defer close(data_out.ch)
|
||||||
|
|
||||||
data_out.Write([]byte(commitId))
|
data_out.Write([]byte(commitId))
|
||||||
data_out.ch <- '\x00'
|
data_out.Write([]byte{0})
|
||||||
var c GitCommit
|
var c GitCommit
|
||||||
c, err = parseGitCommit(data_in.ch)
|
c, err = parseGitCommit(data_in.ch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -973,7 +1108,7 @@ func (e *GitHandlerImpl) GitSubmoduleList(gitPath, commitId string) (submoduleLi
|
|||||||
delete(trees, p)
|
delete(trees, p)
|
||||||
|
|
||||||
data_out.Write([]byte(tree))
|
data_out.Write([]byte(tree))
|
||||||
data_out.ch <- '\x00'
|
data_out.Write([]byte{0})
|
||||||
var tree GitTree
|
var tree GitTree
|
||||||
tree, err = parseGitTree(data_in.ch)
|
tree, err = parseGitTree(data_in.ch)
|
||||||
|
|
||||||
@@ -1010,17 +1145,26 @@ func (e *GitHandlerImpl) GitSubmoduleList(gitPath, commitId string) (submoduleLi
|
|||||||
LogDebug("command run:", cmd.Args)
|
LogDebug("command run:", cmd.Args)
|
||||||
if e := cmd.Run(); e != nil {
|
if e := cmd.Run(); e != nil {
|
||||||
LogError(e)
|
LogError(e)
|
||||||
|
close_done()
|
||||||
close(data_in.ch)
|
close(data_in.ch)
|
||||||
close(data_out.ch)
|
|
||||||
return submoduleList, e
|
return submoduleList, e
|
||||||
}
|
}
|
||||||
|
|
||||||
done.Lock()
|
done.Lock()
|
||||||
|
close_done()
|
||||||
|
close(data_in.ch)
|
||||||
return submoduleList, err
|
return submoduleList, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *GitHandlerImpl) GitSubmoduleCommitId(cwd, packageName, commitId string) (subCommitId string, valid bool) {
|
func (e *GitHandlerImpl) GitSubmoduleCommitId(cwd, packageName, commitId string) (subCommitId string, valid bool) {
|
||||||
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
done_signal := make(chan struct{})
|
||||||
|
var once sync.Once
|
||||||
|
close_done := func() {
|
||||||
|
once.Do(func() {
|
||||||
|
close(done_signal)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
data_in, data_out := ChanIO{make(chan byte), done_signal}, ChanIO{make(chan byte), done_signal}
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
@@ -1036,17 +1180,18 @@ func (e *GitHandlerImpl) GitSubmoduleCommitId(cwd, packageName, commitId string)
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
defer close_done()
|
||||||
defer close(data_out.ch)
|
defer close(data_out.ch)
|
||||||
|
|
||||||
data_out.Write([]byte(commitId))
|
data_out.Write([]byte(commitId))
|
||||||
data_out.ch <- '\x00'
|
data_out.Write([]byte{0})
|
||||||
c, err := parseGitCommit(data_in.ch)
|
c, err := parseGitCommit(data_in.ch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LogError("Error parsing git commit:", err)
|
LogError("Error parsing git commit:", err)
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
data_out.Write([]byte(c.Tree))
|
data_out.Write([]byte(c.Tree))
|
||||||
data_out.ch <- '\x00'
|
data_out.Write([]byte{0})
|
||||||
tree, err := parseGitTree(data_in.ch)
|
tree, err := parseGitTree(data_in.ch)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1078,12 +1223,14 @@ func (e *GitHandlerImpl) GitSubmoduleCommitId(cwd, packageName, commitId string)
|
|||||||
LogDebug("command run:", cmd.Args)
|
LogDebug("command run:", cmd.Args)
|
||||||
if e := cmd.Run(); e != nil {
|
if e := cmd.Run(); e != nil {
|
||||||
LogError(e)
|
LogError(e)
|
||||||
|
close_done()
|
||||||
close(data_in.ch)
|
close(data_in.ch)
|
||||||
close(data_out.ch)
|
|
||||||
return subCommitId, false
|
return subCommitId, false
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
close_done()
|
||||||
|
close(data_in.ch)
|
||||||
return subCommitId, len(subCommitId) > 0
|
return subCommitId, len(subCommitId) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGitClone(t *testing.T) {
|
func TestGitClone(t *testing.T) {
|
||||||
@@ -717,3 +718,44 @@ func TestGitDirectoryListRepro(t *testing.T) {
|
|||||||
t.Errorf("Expected 'subdir' in directory list, got %v", dirs)
|
t.Errorf("Expected 'subdir' in directory list, got %v", dirs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGitDeadlockFix(t *testing.T) {
|
||||||
|
gitDir := t.TempDir()
|
||||||
|
testDir, _ := os.Getwd()
|
||||||
|
|
||||||
|
cmd := exec.Command("/usr/bin/bash", path.Join(testDir, "tsetup.sh"))
|
||||||
|
cmd.Dir = gitDir
|
||||||
|
_, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
|
gh, err := AllocateGitWorkTree(gitDir, "Test", "test@example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
h, err := gh.ReadExistingPath(".")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer h.Close()
|
||||||
|
|
||||||
|
// Use a blob ID to trigger error in GitParseCommits
|
||||||
|
// This ensures that the function returns error immediately and doesn't deadlock
|
||||||
|
blobId := "81aba862107f1e2f5312e165453955485f424612f313d6c2fb1b31fef9f82a14"
|
||||||
|
|
||||||
|
done := make(chan error)
|
||||||
|
go func() {
|
||||||
|
_, err := h.GitParseCommits("", []string{blobId})
|
||||||
|
done <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error from GitParseCommits with blob ID, got nil")
|
||||||
|
} else {
|
||||||
|
// This is expected
|
||||||
|
t.Logf("Got expected error: %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("GitParseCommits deadlocked! Fix is NOT working.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -537,7 +537,7 @@ func ObsSafeProjectName(prjname string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ValidBlockModes []string = []string{"all", "local", "never"}
|
var ValidBlockModes []string = []string{"all", "local", "never"}
|
||||||
var ValidPrjLinkModes []string = []string{"off", "localdep", "alldirect", "all"}
|
var ValidPrjLinkModes []string = []string{"off", "localdep", "alldirect", "alldirect_or_localdep", "all"}
|
||||||
var ValidTriggerModes []string = []string{"transitive", "direct", "local"}
|
var ValidTriggerModes []string = []string{"transitive", "direct", "local"}
|
||||||
|
|
||||||
func (c *ObsClient) SetProjectMeta(meta *ProjectMeta) error {
|
func (c *ObsClient) SetProjectMeta(meta *ProjectMeta) error {
|
||||||
|
|||||||
156
common/pr.go
156
common/pr.go
@@ -554,6 +554,144 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
|
|||||||
return is_manually_reviewed_ok
|
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 {
|
func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
|
||||||
prjgit_info, err := rs.GetPrjGitPR()
|
prjgit_info, err := rs.GetPrjGitPR()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -718,10 +856,8 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
|
|||||||
prinfo.RemoteName, err = git.GitClone(repo.Name, br, repo.SSHURL)
|
prinfo.RemoteName, err = git.GitClone(repo.Name, br, repo.SSHURL)
|
||||||
PanicOnError(err)
|
PanicOnError(err)
|
||||||
git.GitExecOrPanic(repo.Name, "fetch", prinfo.RemoteName, head.Sha)
|
git.GitExecOrPanic(repo.Name, "fetch", prinfo.RemoteName, head.Sha)
|
||||||
|
if rs.Config.MergeMode == MergeModeDevel || isNewRepo {
|
||||||
if isNewRepo {
|
git.GitExecOrPanic(repo.Name, "checkout", "-B", br, head.Sha)
|
||||||
LogInfo("Force-pushing new repository branch", br, "to", head.Sha)
|
|
||||||
// we don't merge, we just set the branch to this commit
|
|
||||||
} else {
|
} else {
|
||||||
git.GitExecOrPanic(repo.Name, "merge", "--ff", head.Sha)
|
git.GitExecOrPanic(repo.Name, "merge", "--ff", head.Sha)
|
||||||
}
|
}
|
||||||
@@ -748,11 +884,15 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !IsDryRun {
|
if !IsDryRun {
|
||||||
if isNewRepo {
|
params := []string{"push"}
|
||||||
git.GitExecOrPanic(repo.Name, "push", "-f", prinfo.RemoteName, prinfo.PR.Head.Sha+":"+prinfo.PR.Base.Name)
|
if rs.Config.MergeMode == MergeModeDevel || isNewRepo {
|
||||||
} else {
|
params = append(params, "-f")
|
||||||
git.GitExecOrPanic(repo.Name, "push", prinfo.RemoteName)
|
|
||||||
}
|
}
|
||||||
|
params = append(params, prinfo.RemoteName)
|
||||||
|
if isNewRepo {
|
||||||
|
params = append(params, prinfo.PR.Head.Sha+":"+prinfo.PR.Base.Name)
|
||||||
|
}
|
||||||
|
git.GitExecOrPanic(repo.Name, params...)
|
||||||
} else {
|
} else {
|
||||||
LogInfo("*** WOULD push", repo.Name, "to", prinfo.RemoteName)
|
LogInfo("*** WOULD push", repo.Name, "to", prinfo.RemoteName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ func TestPRSet_Merge_Special(t *testing.T) {
|
|||||||
// Clone and fetch for new-pkg
|
// Clone and fetch for new-pkg
|
||||||
mockGit.EXPECT().GitClone("new-pkg", "main", "pkg-ssh-url").Return("origin", nil)
|
mockGit.EXPECT().GitClone("new-pkg", "main", "pkg-ssh-url").Return("origin", nil)
|
||||||
mockGit.EXPECT().GitExecOrPanic("new-pkg", "fetch", "origin", "pkg-head-sha")
|
mockGit.EXPECT().GitExecOrPanic("new-pkg", "fetch", "origin", "pkg-head-sha")
|
||||||
|
mockGit.EXPECT().GitExecOrPanic("new-pkg", "checkout", "-B", "main", "pkg-head-sha")
|
||||||
|
|
||||||
// Pushing changes
|
// Pushing changes
|
||||||
mockGit.EXPECT().GitExecOrPanic("_ObsPrj", "push", "origin")
|
mockGit.EXPECT().GitExecOrPanic("_ObsPrj", "push", "origin")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package common_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
@@ -1267,7 +1268,7 @@ func TestPRMerge(t *testing.T) {
|
|||||||
Owner: &models.User{
|
Owner: &models.User{
|
||||||
UserName: "org",
|
UserName: "org",
|
||||||
},
|
},
|
||||||
SSHURL: "file://" + path.Join(repoDir, "prjgit"),
|
SSHURL: "ssh://git@src.opensuse.org/org/prj.git",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Head: &models.PRBranchInfo{
|
Head: &models.PRBranchInfo{
|
||||||
@@ -1289,7 +1290,7 @@ func TestPRMerge(t *testing.T) {
|
|||||||
Owner: &models.User{
|
Owner: &models.User{
|
||||||
UserName: "org",
|
UserName: "org",
|
||||||
},
|
},
|
||||||
SSHURL: "file://" + path.Join(cmd.Dir, "prjgit"),
|
SSHURL: "ssh://git@src.opensuse.org/org/prj.git",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Head: &models.PRBranchInfo{
|
Head: &models.PRBranchInfo{
|
||||||
@@ -1399,3 +1400,345 @@ 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", "fetch", "origin_pkg", pkgPR.Head.Sha)
|
||||||
|
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,10 +92,13 @@ func ConnectToExchangeForPublish(host, username, password string) {
|
|||||||
auth = username + ":" + password + "@"
|
auth = username + ":" + password + "@"
|
||||||
}
|
}
|
||||||
|
|
||||||
connection, err := rabbitmq.DialTLS("amqps://"+auth+host, &tls.Config{
|
connection, err := rabbitmq.DialConfig("amqps://"+auth+host, rabbitmq.Config{
|
||||||
ServerName: host,
|
Dial: rabbitmq.DefaultDial(10 * time.Second),
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
ServerName: host,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
failOnError(err, "Cannot connect to rabbit.opensuse.org")
|
failOnError(err, "Cannot connect to "+host)
|
||||||
defer connection.Close()
|
defer connection.Close()
|
||||||
|
|
||||||
ch, err := connection.Channel()
|
ch, err := connection.Channel()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ ENV container=podman
|
|||||||
|
|
||||||
ENV LANG=en_US.UTF-8
|
ENV LANG=en_US.UTF-8
|
||||||
|
|
||||||
RUN zypper -vvvn install podman podman-compose vim make python3-pytest python3-requests python3-pytest-dependency
|
RUN zypper -vvvn install podman podman-compose vim make python3-pytest python3-requests python3-pytest-dependency python3-pytest-httpserver
|
||||||
|
|
||||||
COPY . /opt/project/
|
COPY . /opt/project/
|
||||||
|
|
||||||
@@ -1,51 +1,19 @@
|
|||||||
# We want to be able to test in two **modes**:
|
|
||||||
# A. bots are used from official packages as defined in */Dockerfile.package
|
|
||||||
# B. bots are just picked up from binaries that are placed in corresponding parent directory.
|
|
||||||
|
|
||||||
# The topology is defined in podman-compose file and can be spawned in two ways:
|
# The topology is defined in podman-compose file and can be spawned in two ways:
|
||||||
# 1. Privileged container (needs no additional dependancies)
|
# 1. podman-compose on a local machine (needs dependencies as defined in the Dockerfile)
|
||||||
# 2. podman-compose on a local machine (needs dependencies as defined in the Dockerfile)
|
# 2. pytest in a dedicated container (recommended)
|
||||||
|
|
||||||
|
|
||||||
# Typical workflow:
|
# Typical workflow:
|
||||||
# A1: - run 'make test_package'
|
# 1. 'make build' - prepares images
|
||||||
# B1: - run 'make test_local' (make sure that the go binaries in parent folder are built)
|
# 2. 'make up' - spawns podman-compose
|
||||||
# A2:
|
# 3. 'make pytest' - run tests inside the tester container
|
||||||
# 1. 'make build_package' - prepares images (recommended, otherwise there might be surprises if image fails to build during `make up`)
|
# 4. 'make down' - once the containers are not needed
|
||||||
# 2. 'make up' - spawns podman-compose
|
#
|
||||||
# 3. 'pytest -v tests/*' - run tests
|
# OR just run 'make test' to do it all at once.
|
||||||
# 4. 'make down' - once the containers are not needed
|
|
||||||
# B2: (make sure the go binaries in the parent folder are built)
|
|
||||||
# 1. 'make build_local' - prepared images (recommended, otherwise there might be surprises if image fails to build during `make up`)
|
|
||||||
# 2. 'make up' - spawns podman-compose
|
|
||||||
# 3. 'pytest -v tests/*' - run tests
|
|
||||||
# 4. 'make down' - once the containers are not needed
|
|
||||||
|
|
||||||
|
|
||||||
AUTO_DETECT_MODE := $(shell if test -e ../workflow-pr/workflow-pr; then echo .local; else echo .package; fi)
|
AUTO_DETECT_MODE := $(shell if test -e ../workflow-pr/workflow-pr; then echo .local; else echo .package; fi)
|
||||||
|
|
||||||
# try to detect mode B1, otherwise mode A1
|
# Default test target
|
||||||
test: GIWTF_IMAGE_SUFFIX=$(AUTO_DETECT_MODE)
|
test: test_b
|
||||||
test: build_container test_container
|
|
||||||
|
|
||||||
# mode A1
|
|
||||||
test_package: GIWTF_IMAGE_SUFFIX=.package
|
|
||||||
test_package: build_container test_container
|
|
||||||
|
|
||||||
# mode B1
|
|
||||||
test_local: GIWTF_IMAGE_SUFFIX=.local
|
|
||||||
test_local: build_container test_container
|
|
||||||
|
|
||||||
MODULES := gitea-events-rabbitmq-publisher obs-staging-bot workflow-pr
|
|
||||||
|
|
||||||
# Prepare topology 1
|
|
||||||
build_container:
|
|
||||||
podman build ../ -f integration/Dockerfile -t autogits_integration
|
|
||||||
|
|
||||||
# Run tests in topology 1
|
|
||||||
test_container:
|
|
||||||
podman run --rm --privileged -t -e GIWTF_IMAGE_SUFFIX=$(GIWTF_IMAGE_SUFFIX) autogits_integration /usr/bin/bash -c "make build && make up && sleep 25 && pytest -v tests/*"
|
|
||||||
|
|
||||||
|
|
||||||
build_local: AUTO_DETECT_MODE=.local
|
build_local: AUTO_DETECT_MODE=.local
|
||||||
build_local: build
|
build_local: build
|
||||||
@@ -53,16 +21,66 @@ build_local: build
|
|||||||
build_package: AUTO_DETECT_MODE=.package
|
build_package: AUTO_DETECT_MODE=.package
|
||||||
build_package: build
|
build_package: build
|
||||||
|
|
||||||
# parse all service images from podman-compose and build them (topology 2)
|
# parse all service images from podman-compose and build them
|
||||||
|
# mode B with pytest in container
|
||||||
|
test_b: AUTO_DETECT_MODE=.local
|
||||||
|
test_b: build up wait_healthy pytest
|
||||||
|
|
||||||
|
# Complete cycle for CI
|
||||||
|
test-ci: test_b down
|
||||||
|
|
||||||
|
wait_healthy:
|
||||||
|
@echo "Waiting for services to be healthy..."
|
||||||
|
@echo "Waiting for gitea (max 2m)..."
|
||||||
|
@start_time=$$(date +%s); \
|
||||||
|
until podman exec gitea-test curl -f -s http://localhost:3000/api/v1/version >/dev/null 2>&1; do \
|
||||||
|
current_time=$$(date +%s); \
|
||||||
|
elapsed=$$((current_time - start_time)); \
|
||||||
|
if [ $$elapsed -gt 120 ]; then \
|
||||||
|
echo "ERROR: Gitea failed to start within 2 minutes."; \
|
||||||
|
echo "--- Troubleshooting Info ---"; \
|
||||||
|
echo "Diagnostics output (curl):"; \
|
||||||
|
podman exec gitea-test curl -v http://localhost:3000/api/v1/version || true; \
|
||||||
|
echo "--- Container Logs ---"; \
|
||||||
|
podman logs gitea-test --tail 20; \
|
||||||
|
echo "--- Container Status ---"; \
|
||||||
|
podman inspect gitea-test --format '{{.State.Status}}'; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
sleep 2; \
|
||||||
|
done
|
||||||
|
@echo "Waiting for rabbitmq (max 2m)..."
|
||||||
|
@start_time=$$(date +%s); \
|
||||||
|
until podman exec rabbitmq-test rabbitmq-diagnostics check_running -q >/dev/null 2>&1; do \
|
||||||
|
current_time=$$(date +%s); \
|
||||||
|
elapsed=$$((current_time - start_time)); \
|
||||||
|
if [ $$elapsed -gt 120 ]; then \
|
||||||
|
echo "ERROR: RabbitMQ failed to start within 2 minutes."; \
|
||||||
|
echo "--- Troubleshooting Info ---"; \
|
||||||
|
echo "Diagnostics output:"; \
|
||||||
|
podman exec rabbitmq-test rabbitmq-diagnostics check_running || true; \
|
||||||
|
echo "--- Container Logs ---"; \
|
||||||
|
podman logs rabbitmq-test --tail 20; \
|
||||||
|
echo "--- Container Status ---"; \
|
||||||
|
podman inspect rabbitmq-test --format '{{.State.Status}}'; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
sleep 2; \
|
||||||
|
done
|
||||||
|
@echo "All services are healthy!"
|
||||||
|
|
||||||
|
pytest:
|
||||||
|
podman-compose exec tester pytest -v tests
|
||||||
|
|
||||||
build:
|
build:
|
||||||
podman pull docker.io/library/rabbitmq:3.13.7-management
|
podman pull docker.io/library/rabbitmq:3.13.7-management
|
||||||
for i in $$(grep -A 1000 services: podman-compose.yml | grep -oE '^ [^: ]+'); do GIWTF_IMAGE_SUFFIX=$(AUTO_DETECT_MODE) podman-compose build $$i || exit 1; done
|
for i in $$(grep -A 1000 services: podman-compose.yml | grep -oE '^ [^: ]+'); do GIWTF_IMAGE_SUFFIX=$(AUTO_DETECT_MODE) podman-compose build $$i || exit 1; done
|
||||||
|
|
||||||
# this will spawn prebuilt containers (topology 2)
|
# this will spawn prebuilt containers
|
||||||
up:
|
up:
|
||||||
podman-compose up -d
|
podman-compose up -d
|
||||||
|
|
||||||
# tear down (topology 2)
|
# tear down
|
||||||
down:
|
down:
|
||||||
podman-compose down
|
podman-compose down
|
||||||
|
|
||||||
@@ -73,4 +91,3 @@ up-bots-package:
|
|||||||
# mode B
|
# mode B
|
||||||
up-bots-local:
|
up-bots-local:
|
||||||
GIWTF_IMAGE_SUFFIX=.local podman-compose up -d
|
GIWTF_IMAGE_SUFFIX=.local podman-compose up -d
|
||||||
|
|
||||||
|
|||||||
52
integration/Makefile.md
Normal file
52
integration/Makefile.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Makefile Targets
|
||||||
|
|
||||||
|
This document describes the targets available in the `integration/Makefile`.
|
||||||
|
|
||||||
|
## Primary Workflow
|
||||||
|
|
||||||
|
### `test` (or `test_b`)
|
||||||
|
- **Action**: Performs a complete build-and-test cycle.
|
||||||
|
- **Steps**:
|
||||||
|
1. `build`: Prepares all container images.
|
||||||
|
2. `up`: Starts all services via `podman-compose`.
|
||||||
|
3. `wait_healthy`: Polls Gitea and RabbitMQ until they are ready.
|
||||||
|
4. `pytest`: Executes the test suite inside the `tester` container.
|
||||||
|
- **Outcome**: The environment remains active for fast iteration.
|
||||||
|
|
||||||
|
### `test-ci`
|
||||||
|
- **Action**: Performs the full `test` cycle followed by teardown.
|
||||||
|
- **Steps**: `test_b` -> `down`
|
||||||
|
- **Purpose**: Ideal for CI environments where a clean state is required after testing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Individual Targets
|
||||||
|
|
||||||
|
### `build`
|
||||||
|
- **Action**: Pulls external images (RabbitMQ) and builds all local service images defined in `podman-compose.yml`.
|
||||||
|
- **Note**: Use `build_local` or `build_package` to specify bot source mode.
|
||||||
|
|
||||||
|
### `up`
|
||||||
|
- **Action**: Starts the container topology in detached mode.
|
||||||
|
|
||||||
|
### `wait_healthy`
|
||||||
|
- **Action**: Polls the health status of `gitea-test` and `rabbitmq-test` containers.
|
||||||
|
- **Purpose**: Ensures infrastructure is stable before test execution.
|
||||||
|
|
||||||
|
### `pytest`
|
||||||
|
- **Action**: Runs `pytest -v tests/*` inside the running `tester` container.
|
||||||
|
- **Requirement**: The environment must already be started via `up`.
|
||||||
|
|
||||||
|
### `down`
|
||||||
|
- **Action**: Stops and removes all containers and networks defined in the compose file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Modes
|
||||||
|
|
||||||
|
The Makefile supports two deployment modes via `GIWTF_IMAGE_SUFFIX`:
|
||||||
|
|
||||||
|
- **.local** (Default): Uses binaries built from the local source (requires `make build` in project root).
|
||||||
|
- **.package**: Uses official pre-built packages for the bots.
|
||||||
|
|
||||||
|
Targets like `build_local`, `build_package`, `up-bots-local`, and `up-bots-package` allow for explicit mode selection.
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
+-------------------------------------------------------------------------------------------------+
|
|
||||||
| Makefile Targets |
|
|
||||||
+-------------------------------------------------------------------------------------------------+
|
|
||||||
| |
|
|
||||||
| [Default Test Workflow] |
|
|
||||||
| test (Auto-detects mode: .local or .package) |
|
|
||||||
| └─> build_container |
|
|
||||||
| └─> test_container |
|
|
||||||
| |
|
|
||||||
| [Specific Test Workflows - Topology 1: Privileged Container] |
|
|
||||||
| test_package (Mode A1: Bots from official packages) |
|
|
||||||
| └─> build_container |
|
|
||||||
| └─> test_container |
|
|
||||||
| |
|
|
||||||
| test_local (Mode B1: Bots from local binaries) |
|
|
||||||
| └─> build_container |
|
|
||||||
| └─> test_container |
|
|
||||||
| |
|
|
||||||
| build_container |
|
|
||||||
| - Action: Builds the `autogits_integration` privileged container image. |
|
|
||||||
| - Purpose: Prepares an environment for running tests within a single container. |
|
|
||||||
| |
|
|
||||||
| test_container |
|
|
||||||
| - Action: Runs `autogits_integration` container, executes `make build`, `make up`, and |
|
|
||||||
| `pytest -v tests/*` inside it. |
|
|
||||||
| - Purpose: Executes the full test suite in Topology 1 (privileged container). |
|
|
||||||
| |
|
|
||||||
| [Build & Orchestration Workflows - Topology 2: podman-compose] |
|
|
||||||
| |
|
|
||||||
| build_package (Mode A: Builds service images from official packages) |
|
|
||||||
| └─> build |
|
|
||||||
| |
|
|
||||||
| build_local (Mode B: Builds service images from local binaries) |
|
|
||||||
| └─> build |
|
|
||||||
| |
|
|
||||||
| build |
|
|
||||||
| - Action: Pulls `rabbitmq` image and iterates through `podman-compose.yml` services |
|
|
||||||
| to build each one. |
|
|
||||||
| - Purpose: Prepares all necessary service images for Topology 2 deployment. |
|
|
||||||
| |
|
|
||||||
| up |
|
|
||||||
| - Action: Starts all services defined in `podman-compose.yml` in detached mode. |
|
|
||||||
| - Purpose: Deploys the application topology (containers) for testing or development. |
|
|
||||||
| |
|
|
||||||
| down |
|
|
||||||
| - Action: Stops and removes all services started by `up`. |
|
|
||||||
| - Purpose: Cleans up the deployed application topology. |
|
|
||||||
| |
|
|
||||||
| up-bots-package (Mode A: Spawns Topology 2 with official package bots) |
|
|
||||||
| - Action: Calls `podman-compose up -d` with `GIWTF_IMAGE_SUFFIX=.package`. |
|
|
||||||
| - Purpose: Specifically brings up the environment using official package bots. |
|
|
||||||
| |
|
|
||||||
| up-bots-local (Mode B: Spawns Topology 2 with local binaries) |
|
|
||||||
| - Action: Calls `podman-compose up -d` with `GIWTF_IMAGE_SUFFIX=.local`. |
|
|
||||||
| - Purpose: Specifically brings up the environment using local binaries. |
|
|
||||||
| |
|
|
||||||
+-------------------------------------------------------------------------------------------------+
|
|
||||||
7
integration/gwf-test-basecontainer/Dockerfile
Normal file
7
integration/gwf-test-basecontainer/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#!BuildTag: openbuildservice/gwf-test-basecontainer
|
||||||
|
#!UseOBSRepositories
|
||||||
|
FROM registry.suse.com/bci/bci-base:15.7
|
||||||
|
|
||||||
|
RUN zypper -n install binutils gawk git git-core git-lfs jq make openssh openssh-clients podman podman-compose python311-pytest python311-pytest-dependency python311-pytest-httpserver python311-requests sqlite3 vim which
|
||||||
|
|
||||||
|
RUN zypper -n install autogits-gitea-events-rabbitmq-publisher autogits-obs-staging-bot autogits-workflow-pr gitea
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Use a base Python image
|
|
||||||
FROM registry.suse.com/bci/python:3.11
|
|
||||||
|
|
||||||
# Set the working directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy the server script
|
|
||||||
COPY server.py .
|
|
||||||
|
|
||||||
# Expose the port the server will run on
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
# Command to run the server
|
|
||||||
CMD ["python3", "-u", "server.py"]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<project name="openSUSE:Leap:16.0:PullRequest">
|
|
||||||
<title>Leap 16.0 PullRequest area</title>
|
|
||||||
<description>Base project to define the pull request builds</description>
|
|
||||||
<person userid="autogits_obs_staging_bot" role="maintainer"/>
|
|
||||||
<person userid="maxlin_factory" role="maintainer"/>
|
|
||||||
<group groupid="maintenance-opensuse.org" role="maintainer"/>
|
|
||||||
<debuginfo>
|
|
||||||
<enable/>
|
|
||||||
</debuginfo>
|
|
||||||
<repository name="standard">
|
|
||||||
<path project="openSUSE:Leap:16.0" repository="standard"/>
|
|
||||||
<arch>x86_64</arch>
|
|
||||||
<arch>i586</arch>
|
|
||||||
<arch>aarch64</arch>
|
|
||||||
<arch>ppc64le</arch>
|
|
||||||
<arch>s390x</arch>
|
|
||||||
</repository>
|
|
||||||
</project>
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
<project name="openSUSE:Leap:16.0">
|
|
||||||
<title>openSUSE Leap 16.0 based on SLFO</title>
|
|
||||||
<description>Leap 16.0 based on SLES 16.0 (specifically SLFO:1.2)</description>
|
|
||||||
<link project="openSUSE:Backports:SLE-16.0"/>
|
|
||||||
<scmsync>http://gitea-test:3000/myproducts/mySLFO#staging-main</scmsync>
|
|
||||||
<person userid="dimstar_suse" role="maintainer"/>
|
|
||||||
<person userid="lkocman-factory" role="maintainer"/>
|
|
||||||
<person userid="maxlin_factory" role="maintainer"/>
|
|
||||||
<person userid="factory-auto" role="reviewer"/>
|
|
||||||
<person userid="licensedigger" role="reviewer"/>
|
|
||||||
<group groupid="autobuild-team" role="maintainer"/>
|
|
||||||
<group groupid="factory-maintainers" role="maintainer"/>
|
|
||||||
<group groupid="maintenance-opensuse.org" role="maintainer"/>
|
|
||||||
<group groupid="factory-staging" role="reviewer"/>
|
|
||||||
<build>
|
|
||||||
<disable repository="ports"/>
|
|
||||||
</build>
|
|
||||||
<debuginfo>
|
|
||||||
<enable/>
|
|
||||||
</debuginfo>
|
|
||||||
<repository name="standard" rebuild="local">
|
|
||||||
<path project="openSUSE:Backports:SLE-16.0" repository="standard"/>
|
|
||||||
<path project="SUSE:SLFO:1.2" repository="standard"/>
|
|
||||||
<arch>local</arch>
|
|
||||||
<arch>i586</arch>
|
|
||||||
<arch>x86_64</arch>
|
|
||||||
<arch>aarch64</arch>
|
|
||||||
<arch>ppc64le</arch>
|
|
||||||
<arch>s390x</arch>
|
|
||||||
</repository>
|
|
||||||
<repository name="product">
|
|
||||||
<releasetarget project="openSUSE:Leap:16.0:ToTest" repository="product" trigger="manual"/>
|
|
||||||
<path project="openSUSE:Leap:16.0:NonFree" repository="standard"/>
|
|
||||||
<path project="openSUSE:Leap:16.0" repository="images"/>
|
|
||||||
<path project="openSUSE:Leap:16.0" repository="standard"/>
|
|
||||||
<path project="openSUSE:Backports:SLE-16.0" repository="standard"/>
|
|
||||||
<path project="SUSE:SLFO:1.2" repository="standard"/>
|
|
||||||
<arch>local</arch>
|
|
||||||
<arch>i586</arch>
|
|
||||||
<arch>x86_64</arch>
|
|
||||||
<arch>aarch64</arch>
|
|
||||||
<arch>ppc64le</arch>
|
|
||||||
<arch>s390x</arch>
|
|
||||||
</repository>
|
|
||||||
<repository name="ports">
|
|
||||||
<arch>armv7l</arch>
|
|
||||||
</repository>
|
|
||||||
<repository name="images">
|
|
||||||
<releasetarget project="openSUSE:Leap:16.0:ToTest" repository="images" trigger="manual"/>
|
|
||||||
<path project="openSUSE:Leap:16.0" repository="standard"/>
|
|
||||||
<path project="openSUSE:Backports:SLE-16.0" repository="standard"/>
|
|
||||||
<path project="SUSE:SLFO:1.2" repository="standard"/>
|
|
||||||
<arch>i586</arch>
|
|
||||||
<arch>x86_64</arch>
|
|
||||||
<arch>aarch64</arch>
|
|
||||||
<arch>ppc64le</arch>
|
|
||||||
<arch>s390x</arch>
|
|
||||||
</repository>
|
|
||||||
</project>
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
import http.server
|
|
||||||
import socketserver
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
import signal
|
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
import fnmatch
|
|
||||||
|
|
||||||
PORT = 8080
|
|
||||||
RESPONSE_DIR = "/app/responses"
|
|
||||||
STATE_DIR = "/tmp/mock_obs_state"
|
|
||||||
|
|
||||||
class MockOBSHandler(http.server.SimpleHTTPRequestHandler):
|
|
||||||
def do_GET(self):
|
|
||||||
logging.info(f"GET request for: {self.path}")
|
|
||||||
path_without_query = self.path.split('?')[0]
|
|
||||||
|
|
||||||
# Check for state stored by a PUT request first
|
|
||||||
sanitized_put_path = 'PUT' + path_without_query.replace('/', '_')
|
|
||||||
state_file_path = os.path.join(STATE_DIR, sanitized_put_path)
|
|
||||||
if os.path.exists(state_file_path):
|
|
||||||
logging.info(f"Found stored PUT state for {self.path} at {state_file_path}")
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header("Content-type", "application/xml")
|
|
||||||
file_size = os.path.getsize(state_file_path)
|
|
||||||
self.send_header("Content-Length", str(file_size))
|
|
||||||
self.end_headers()
|
|
||||||
with open(state_file_path, 'rb') as f:
|
|
||||||
self.wfile.write(f.read())
|
|
||||||
return
|
|
||||||
|
|
||||||
# If no PUT state file, fall back to the glob/exact match logic
|
|
||||||
self.handle_request('GET')
|
|
||||||
|
|
||||||
def do_PUT(self):
|
|
||||||
logging.info(f"PUT request for: {self.path}")
|
|
||||||
logging.info(f"Headers: {self.headers}")
|
|
||||||
path_without_query = self.path.split('?')[0]
|
|
||||||
|
|
||||||
body = b''
|
|
||||||
if self.headers.get('Transfer-Encoding', '').lower() == 'chunked':
|
|
||||||
logging.info("Chunked transfer encoding detected")
|
|
||||||
while True:
|
|
||||||
line = self.rfile.readline().strip()
|
|
||||||
if not line:
|
|
||||||
break
|
|
||||||
chunk_length = int(line, 16)
|
|
||||||
if chunk_length == 0:
|
|
||||||
self.rfile.readline()
|
|
||||||
break
|
|
||||||
body += self.rfile.read(chunk_length)
|
|
||||||
self.rfile.read(2) # Read the trailing CRLF
|
|
||||||
else:
|
|
||||||
content_length = int(self.headers.get('Content-Length', 0))
|
|
||||||
body = self.rfile.read(content_length)
|
|
||||||
|
|
||||||
logging.info(f"Body: {body.decode('utf-8')}")
|
|
||||||
sanitized_path = 'PUT' + path_without_query.replace('/', '_')
|
|
||||||
state_file_path = os.path.join(STATE_DIR, sanitized_path)
|
|
||||||
|
|
||||||
logging.info(f"Saving state for {self.path} to {state_file_path}")
|
|
||||||
os.makedirs(os.path.dirname(state_file_path), exist_ok=True)
|
|
||||||
with open(state_file_path, 'wb') as f:
|
|
||||||
f.write(body)
|
|
||||||
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header("Content-type", "text/plain")
|
|
||||||
response_body = b"OK"
|
|
||||||
self.send_header("Content-Length", str(len(response_body)))
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(response_body)
|
|
||||||
|
|
||||||
def do_POST(self):
|
|
||||||
logging.info(f"POST request for: {self.path}")
|
|
||||||
self.handle_request('POST')
|
|
||||||
|
|
||||||
def do_DELETE(self):
|
|
||||||
logging.info(f"DELETE request for: {self.path}")
|
|
||||||
self.handle_request('DELETE')
|
|
||||||
|
|
||||||
def handle_request(self, method):
|
|
||||||
path_without_query = self.path.split('?')[0]
|
|
||||||
sanitized_request_path = method + path_without_query.replace('/', '_')
|
|
||||||
logging.info(f"Handling request, looking for match for: {sanitized_request_path}")
|
|
||||||
|
|
||||||
response_file = None
|
|
||||||
# Check for glob match first
|
|
||||||
if os.path.exists(RESPONSE_DIR):
|
|
||||||
for filename in os.listdir(RESPONSE_DIR):
|
|
||||||
if fnmatch.fnmatch(sanitized_request_path, filename):
|
|
||||||
response_file = os.path.join(RESPONSE_DIR, filename)
|
|
||||||
logging.info(f"Found matching response file (glob): {response_file}")
|
|
||||||
break
|
|
||||||
|
|
||||||
# Fallback to exact match if no glob match
|
|
||||||
if response_file is None:
|
|
||||||
exact_file = os.path.join(RESPONSE_DIR, sanitized_request_path)
|
|
||||||
if os.path.exists(exact_file):
|
|
||||||
response_file = exact_file
|
|
||||||
logging.info(f"Found matching response file (exact): {response_file}")
|
|
||||||
|
|
||||||
if response_file:
|
|
||||||
logging.info(f"Serving content from {response_file}")
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header("Content-type", "application/xml")
|
|
||||||
file_size = os.path.getsize(response_file)
|
|
||||||
self.send_header("Content-Length", str(file_size))
|
|
||||||
self.end_headers()
|
|
||||||
with open(response_file, 'rb') as f:
|
|
||||||
self.wfile.write(f.read())
|
|
||||||
else:
|
|
||||||
logging.info(f"Response file not found for {sanitized_request_path}. Sending 404.")
|
|
||||||
self.send_response(404)
|
|
||||||
self.send_header("Content-type", "text/plain")
|
|
||||||
body = f"Mock response not found for {sanitized_request_path}".encode('utf-8')
|
|
||||||
self.send_header("Content-Length", str(len(body)))
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(body)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
|
|
||||||
|
|
||||||
if not os.path.exists(STATE_DIR):
|
|
||||||
logging.info(f"Creating state directory: {STATE_DIR}")
|
|
||||||
os.makedirs(STATE_DIR)
|
|
||||||
if not os.path.exists(RESPONSE_DIR):
|
|
||||||
os.makedirs(RESPONSE_DIR)
|
|
||||||
|
|
||||||
with socketserver.TCPServer(("", PORT), MockOBSHandler) as httpd:
|
|
||||||
logging.info(f"Serving mock OBS API on port {PORT}")
|
|
||||||
|
|
||||||
def graceful_shutdown(sig, frame):
|
|
||||||
logging.info("Received SIGTERM, shutting down gracefully...")
|
|
||||||
threading.Thread(target=httpd.shutdown).start()
|
|
||||||
|
|
||||||
signal.signal(signal.SIGTERM, graceful_shutdown)
|
|
||||||
|
|
||||||
httpd.serve_forever()
|
|
||||||
logging.info("Server has shut down.")
|
|
||||||
64
integration/podman-compose.md
Normal file
64
integration/podman-compose.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Podman-Compose Services Architecture
|
||||||
|
|
||||||
|
This document describes the services defined in `podman-compose.yml` used for integration testing.
|
||||||
|
|
||||||
|
## Network
|
||||||
|
- **gitea-network**: A bridge network that enables communication between all services.
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
### gitea
|
||||||
|
- **Description**: Self-hosted Git service, serving as the central hub for repositories.
|
||||||
|
- **Container Name**: `gitea-test`
|
||||||
|
- **Image**: Built from `./gitea/Dockerfile`
|
||||||
|
- **Ports**: `3000` (HTTP), `3022` (SSH)
|
||||||
|
- **Volumes**: `./gitea-data` (persistent data), `./gitea-logs` (logs)
|
||||||
|
- **Healthcheck**: Monitors the Gitea API version endpoint.
|
||||||
|
|
||||||
|
### rabbitmq
|
||||||
|
- **Description**: Message broker for asynchronous communication between services.
|
||||||
|
- **Container Name**: `rabbitmq-test`
|
||||||
|
- **Image**: `rabbitmq:3.13.7-management`
|
||||||
|
- **Ports**: `5671` (AMQP with TLS), `15672` (Management UI)
|
||||||
|
- **Volumes**: `./rabbitmq-data`, `./rabbitmq-config/certs`, `./rabbitmq-config/rabbitmq.conf`, `./rabbitmq-config/definitions.json`
|
||||||
|
- **Healthcheck**: Ensures the broker is running and ready to accept connections.
|
||||||
|
|
||||||
|
### gitea-publisher
|
||||||
|
- **Description**: Publishes events from Gitea webhooks to the RabbitMQ message queue.
|
||||||
|
- **Container Name**: `gitea-publisher`
|
||||||
|
- **Dependencies**: `gitea` (started), `rabbitmq` (healthy)
|
||||||
|
- **Topic Domain**: `suse`
|
||||||
|
|
||||||
|
### workflow-pr
|
||||||
|
- **Description**: Manages pull request workflows, synchronizing between ProjectGit and PackageGit.
|
||||||
|
- **Container Name**: `workflow-pr`
|
||||||
|
- **Dependencies**: `gitea` (started), `rabbitmq` (healthy)
|
||||||
|
- **Environment**: Configured via `AUTOGITS_*` variables.
|
||||||
|
- **Volumes**: `./gitea-data` (read-only), `./workflow-pr/workflow-pr.json` (config), `./workflow-pr-repos` (working directories)
|
||||||
|
|
||||||
|
### tester
|
||||||
|
- **Description**: The dedicated test runner container. It hosts the `pytest` suite and provides a mock OBS API using `pytest-httpserver`.
|
||||||
|
- **Container Name**: `tester`
|
||||||
|
- **Image**: Built from `./Dockerfile.tester`
|
||||||
|
- **Mock API**: Listens on port `8080` within the container network to simulate OBS.
|
||||||
|
- **Volumes**: Project root mounted at `/opt/project` for source access.
|
||||||
|
|
||||||
|
### obs-staging-bot
|
||||||
|
- **Description**: Interacts with Gitea and the OBS API (mocked by `tester`) to manage staging projects.
|
||||||
|
- **Container Name**: `obs-staging-bot`
|
||||||
|
- **Dependencies**: `gitea` (started), `tester` (started)
|
||||||
|
- **Environment**:
|
||||||
|
- `AUTOGITS_STAGING_BOT_POLL_INTERVAL`: Set to `2s` for fast integration testing.
|
||||||
|
- **Mock Integration**: Points to `http://tester:8080` for both OBS API and Web hosts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Workflow
|
||||||
|
|
||||||
|
1. **Build**: `make build` (root) then `make build` (integration).
|
||||||
|
2. **Up**: `make up` starts all services.
|
||||||
|
3. **Wait**: `make wait_healthy` ensures infrastructure is ready.
|
||||||
|
4. **Test**: `make pytest` runs the suite inside the `tester` container.
|
||||||
|
5. **Down**: `make down` stops and removes containers.
|
||||||
|
|
||||||
|
Use `make test` to perform steps 1-4 automatically.
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
+-------------------------------------------------------------------------------------------------+
|
|
||||||
| Podman-Compose Services Diagram |
|
|
||||||
+-------------------------------------------------------------------------------------------------+
|
|
||||||
| |
|
|
||||||
| [Network] |
|
|
||||||
| gitea-network (Bridge network for inter-service communication) |
|
|
||||||
| |
|
|
||||||
|-------------------------------------------------------------------------------------------------|
|
|
||||||
| |
|
|
||||||
| [Service: gitea] |
|
|
||||||
| Description: Self-hosted Git service, central hub for repositories and code management. |
|
|
||||||
| Container Name: gitea-test |
|
|
||||||
| Image: Built from ./gitea Dockerfile |
|
|
||||||
| Ports: 3000 (HTTP), 3022 (SSH) |
|
|
||||||
| Volumes: ./gitea-data (for persistent data), ./gitea-logs (for logs) |
|
|
||||||
| Network: gitea-network |
|
|
||||||
| |
|
|
||||||
|-------------------------------------------------------------------------------------------------|
|
|
||||||
| |
|
|
||||||
| [Service: rabbitmq] |
|
|
||||||
| Description: Message broker for asynchronous communication between services. |
|
|
||||||
| Container Name: rabbitmq-test |
|
|
||||||
| Image: rabbitmq:3.13.7-management |
|
|
||||||
| Ports: 5671 (AMQP), 15672 (Management UI) |
|
|
||||||
| Volumes: ./rabbitmq-data (for persistent data), ./rabbitmq-config/certs (TLS certs), |
|
|
||||||
| ./rabbitmq-config/rabbitmq.conf (config), ./rabbitmq-config/definitions.json (exchanges)|
|
|
||||||
| Healthcheck: Ensures RabbitMQ is running and healthy. |
|
|
||||||
| Network: gitea-network |
|
|
||||||
| |
|
|
||||||
|-------------------------------------------------------------------------------------------------|
|
|
||||||
| |
|
|
||||||
| [Service: gitea-publisher] |
|
|
||||||
| Description: Publishes events from Gitea to the RabbitMQ message queue. |
|
|
||||||
| Container Name: gitea-publisher |
|
|
||||||
| Image: Built from ../gitea-events-rabbitmq-publisher/Dockerfile (local/package) |
|
|
||||||
| Dependencies: gitea (started), rabbitmq (healthy) |
|
|
||||||
| Environment: RABBITMQ_HOST, RABBITMQ_USERNAME, RABBITMQ_PASSWORD, SSL_CERT_FILE |
|
|
||||||
| Command: Listens for Gitea events, publishes to 'suse' topic, debug enabled. |
|
|
||||||
| Network: gitea-network |
|
|
||||||
| |
|
|
||||||
|-------------------------------------------------------------------------------------------------|
|
|
||||||
| |
|
|
||||||
| [Service: workflow-pr] |
|
|
||||||
| Description: Manages pull request workflows, likely consuming events from RabbitMQ and |
|
|
||||||
| interacting with Gitea. |
|
|
||||||
| Container Name: workflow-pr |
|
|
||||||
| Image: Built from ../workflow-pr/Dockerfile (local/package) |
|
|
||||||
| Dependencies: gitea (started), rabbitmq (healthy) |
|
|
||||||
| Environment: AMQP_USERNAME, AMQP_PASSWORD, SSL_CERT_FILE |
|
|
||||||
| Volumes: ./gitea-data (read-only), ./workflow-pr/workflow-pr.json (config), |
|
|
||||||
| ./workflow-pr-repos (for repositories) |
|
|
||||||
| Command: Configures Gitea/RabbitMQ URLs, enables debug, manages repositories. |
|
|
||||||
| Network: gitea-network |
|
|
||||||
| |
|
|
||||||
|-------------------------------------------------------------------------------------------------|
|
|
||||||
| |
|
|
||||||
| [Service: mock-obs] |
|
|
||||||
| Description: A mock (simulated) service for the Open Build Service (OBS) for testing. |
|
|
||||||
| Container Name: mock-obs |
|
|
||||||
| Image: Built from ./mock-obs Dockerfile |
|
|
||||||
| Ports: 8080 |
|
|
||||||
| Volumes: ./mock-obs/responses (for mock API responses) |
|
|
||||||
| Network: gitea-network |
|
|
||||||
| |
|
|
||||||
|-------------------------------------------------------------------------------------------------|
|
|
||||||
| |
|
|
||||||
| [Service: obs-staging-bot] |
|
|
||||||
| Description: A bot that interacts with Gitea and the mock OBS, likely for staging processes. |
|
|
||||||
| Container Name: obs-staging-bot |
|
|
||||||
| Image: Built from ../obs-staging-bot/Dockerfile (local/package) |
|
|
||||||
| Dependencies: gitea (started), mock-obs (started) |
|
|
||||||
| Environment: OBS_USER, OBS_PASSWORD |
|
|
||||||
| Volumes: ./gitea-data (read-only) |
|
|
||||||
| Command: Configures Gitea/OBS URLs, enables debug. |
|
|
||||||
| Network: gitea-network |
|
|
||||||
| |
|
|
||||||
+-------------------------------------------------------------------------------------------------+
|
|
||||||
@@ -81,6 +81,11 @@ services:
|
|||||||
init: true
|
init: true
|
||||||
networks:
|
networks:
|
||||||
- gitea-network
|
- gitea-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pgrep workflow-pr && [ $$(awk '{print $22}' /proc/$$(pgrep workflow-pr)/stat) -lt $$(($$(awk '{print $1}' /proc/uptime | cut -d. -f1)*100 - 1000)) ]"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
depends_on:
|
depends_on:
|
||||||
gitea:
|
gitea:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
@@ -95,7 +100,7 @@ services:
|
|||||||
- ./workflow-pr/workflow-pr.json:/etc/workflow-pr.json:ro,z
|
- ./workflow-pr/workflow-pr.json:/etc/workflow-pr.json:ro,z
|
||||||
- ./workflow-pr-repos:/var/lib/workflow-pr/repos:Z
|
- ./workflow-pr-repos:/var/lib/workflow-pr/repos:Z
|
||||||
command: [
|
command: [
|
||||||
"-check-on-start",
|
"-check-on-start",
|
||||||
"-debug",
|
"-debug",
|
||||||
"-gitea-url", "http://gitea-test:3000",
|
"-gitea-url", "http://gitea-test:3000",
|
||||||
"-url", "amqps://rabbitmq-test:5671",
|
"-url", "amqps://rabbitmq-test:5671",
|
||||||
@@ -104,17 +109,21 @@ services:
|
|||||||
]
|
]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
mock-obs:
|
tester:
|
||||||
build: ./mock-obs
|
build:
|
||||||
container_name: mock-obs
|
context: .
|
||||||
|
dockerfile: Dockerfile.tester
|
||||||
|
container_name: tester
|
||||||
init: true
|
init: true
|
||||||
|
dns_search: .
|
||||||
networks:
|
networks:
|
||||||
- gitea-network
|
- gitea-network
|
||||||
ports:
|
environment:
|
||||||
- "8080:8080"
|
- PYTEST_HTTPSERVER_HOST=0.0.0.0
|
||||||
|
- PYTEST_HTTPSERVER_PORT=8080
|
||||||
volumes:
|
volumes:
|
||||||
- ./mock-obs/responses:/app/responses:z # Use :z for shared SELinux label
|
- ..:/opt/project:z
|
||||||
restart: unless-stopped
|
command: sleep infinity
|
||||||
|
|
||||||
obs-staging-bot:
|
obs-staging-bot:
|
||||||
build:
|
build:
|
||||||
@@ -125,18 +134,17 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- gitea-network
|
- gitea-network
|
||||||
depends_on:
|
depends_on:
|
||||||
gitea:
|
workflow-pr:
|
||||||
condition: service_started
|
|
||||||
mock-obs:
|
|
||||||
condition: service_started
|
condition: service_started
|
||||||
environment:
|
environment:
|
||||||
- OBS_USER=mock
|
- OBS_USER=mock
|
||||||
- OBS_PASSWORD=mock-long-password
|
- OBS_PASSWORD=mock-long-password
|
||||||
|
- AUTOGITS_STAGING_BOT_POLL_INTERVAL=2s
|
||||||
volumes:
|
volumes:
|
||||||
- ./gitea-data:/gitea-data:ro,z
|
- ./gitea-data:/gitea-data:ro,z
|
||||||
command:
|
command:
|
||||||
- "-debug"
|
- "-debug"
|
||||||
- "-gitea-url=http://gitea-test:3000"
|
- "-gitea-url=http://gitea-test:3000"
|
||||||
- "-obs=http://mock-obs:8080"
|
- "-obs=http://tester:8080"
|
||||||
- "-obs-web=http://mock-obs:8080"
|
- "-obs-web=http://tester:8080"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -7,4 +7,10 @@ markers =
|
|||||||
t005: Test case 005
|
t005: Test case 005
|
||||||
t006: Test case 006
|
t006: Test case 006
|
||||||
t007: Test case 007
|
t007: Test case 007
|
||||||
|
t008: Test case 008
|
||||||
|
t009: Test case 009
|
||||||
|
t010: Test case 010
|
||||||
|
t011: Test case 011
|
||||||
|
t012: Test case 012
|
||||||
|
t013: Test case 013
|
||||||
dependency: pytest-dependency marker
|
dependency: pytest-dependency marker
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ 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-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-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-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-MERGE-012** | P | **MergeMode: devel (No Conflict, Fast-forward)** | 1. Set `MergeMode = "devel"`.<br>2. Create a FF-mergeable PackageGit PR.<br>3. Approve reviews. | 1. Both PRs are merged.<br>2. The package branch HEAD matches the PR head (FF). | High |
|
||||||
|
| **TC-MERGE-013** | P | **MergeMode: replace (No Conflict, Fast-forward)** | 1. Set `MergeMode = "replace"`.<br>2. Create a FF-mergeable PackageGit PR.<br>3. Approve reviews. | 1. Both PRs are merged.<br>2. The package branch HEAD matches the PR head (FF). | 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-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-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 |
|
| **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 |
|
||||||
|
|||||||
@@ -8,7 +8,83 @@ import time
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import base64
|
import base64
|
||||||
from tests.lib.common_test_utils import GiteaAPIClient
|
import re
|
||||||
|
from tests.lib.common_test_utils import GiteaAPIClient, vprint
|
||||||
|
import tests.lib.common_test_utils as common_utils
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def is_test_run():
|
||||||
|
common_utils.IS_TEST_RUN = True
|
||||||
|
yield
|
||||||
|
common_utils.IS_TEST_RUN = False
|
||||||
|
|
||||||
|
if os.environ.get("AUTOGITS_PRINT_FIXTURES") is None:
|
||||||
|
print("--- Fixture messages are suppressed. Set AUTOGITS_PRINT_FIXTURES=1 to enable them. ---")
|
||||||
|
|
||||||
|
class ObsMockState:
|
||||||
|
def __init__(self):
|
||||||
|
self.build_results = {} # project -> (package, code)
|
||||||
|
self.project_metas = {} # project -> scmsync
|
||||||
|
self.default_build_result = None
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def obs_mock_state():
|
||||||
|
return ObsMockState()
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def default_obs_handlers(httpserver, obs_mock_state):
|
||||||
|
"""
|
||||||
|
Sets up default handlers for OBS API to avoid 404s.
|
||||||
|
"""
|
||||||
|
def project_meta_handler(request):
|
||||||
|
project = request.path.split("/")[2]
|
||||||
|
scmsync = obs_mock_state.project_metas.get(project, "http://gitea-test:3000/myproducts/mySLFO.git")
|
||||||
|
return f'<project name="{project}"><scmsync>{scmsync}</scmsync></project>'
|
||||||
|
|
||||||
|
def build_result_handler(request):
|
||||||
|
project = request.path.split("/")[2]
|
||||||
|
res = obs_mock_state.build_results.get(project) or obs_mock_state.default_build_result
|
||||||
|
|
||||||
|
if not res:
|
||||||
|
return '<resultlist></resultlist>'
|
||||||
|
|
||||||
|
package_name, code = res
|
||||||
|
|
||||||
|
# We'll use a simple hardcoded XML here to avoid re-parsing template every time
|
||||||
|
# or we can use the template. For simplicity, let's use a basic one.
|
||||||
|
xml_template = f"""<resultlist state="mock">
|
||||||
|
<result project="{project}" repository="standard" arch="x86_64" code="unpublished" state="unpublished">
|
||||||
|
<scmsync>http://gitea-test:3000/myproducts/mySLFO.git?onlybuild={package_name}#sha</scmsync>
|
||||||
|
<status package="{package_name}" code="{code}"/>
|
||||||
|
</result>
|
||||||
|
</resultlist>"""
|
||||||
|
return xml_template
|
||||||
|
|
||||||
|
# Register handlers
|
||||||
|
httpserver.expect_request(re.compile(r"/source/[^/]+/_meta$"), method="GET").respond_with_handler(project_meta_handler)
|
||||||
|
httpserver.expect_request(re.compile(r"/build/[^/]+/_result"), method="GET").respond_with_handler(build_result_handler)
|
||||||
|
httpserver.expect_request(re.compile(r"/source/[^/]+/_meta$"), method="PUT").respond_with_data("OK")
|
||||||
|
httpserver.expect_request(re.compile(r"/source/[^/]+$"), method="DELETE").respond_with_data("OK")
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_build_result(obs_mock_state):
|
||||||
|
"""
|
||||||
|
Fixture to set up mock build results.
|
||||||
|
"""
|
||||||
|
def _setup_mock(package_name: str, code: str, project: str = None):
|
||||||
|
if project:
|
||||||
|
obs_mock_state.build_results[project] = (package_name, code)
|
||||||
|
else:
|
||||||
|
# If no project specified, we can't easily know which one to set
|
||||||
|
# but usually it's the one the bot will request.
|
||||||
|
# We'll use a special key to signify "all" or we can just wait for the request.
|
||||||
|
# For now, let's assume we want to match openSUSE:Leap:16.0:PullRequest:*
|
||||||
|
# The test will call it with specific project if needed.
|
||||||
|
# In test_pr_workflow, it doesn't know the PR number yet.
|
||||||
|
# So we'll make the handler fallback to this if project not found.
|
||||||
|
obs_mock_state.default_build_result = (package_name, code)
|
||||||
|
|
||||||
|
return _setup_mock
|
||||||
|
|
||||||
BRANCH_CONFIG_COMMON = {
|
BRANCH_CONFIG_COMMON = {
|
||||||
"workflow.config": {
|
"workflow.config": {
|
||||||
@@ -71,6 +147,25 @@ BRANCH_CONFIG_CUSTOM = {
|
|||||||
"ReviewPending": "review/Pending"
|
"ReviewPending": "review/Pending"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"merge-ff": {
|
||||||
|
"workflow.config": {
|
||||||
|
"MergeMode": "ff-only"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"merge-replace": {
|
||||||
|
"workflow.config": {
|
||||||
|
"MergeMode": "replace"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"merge-devel": {
|
||||||
|
"workflow.config": {
|
||||||
|
"MergeMode": "devel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zz-ready-to-start": {
|
||||||
|
"workflow.config": {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +176,7 @@ _CREATED_USERS = set()
|
|||||||
_CREATED_LABELS = set()
|
_CREATED_LABELS = set()
|
||||||
_ADDED_COLLABORATORS = set() # format: (org_repo, username)
|
_ADDED_COLLABORATORS = set() # format: (org_repo, username)
|
||||||
|
|
||||||
def setup_users_from_config(client: GiteaAPIClient, wf: dict, mt: dict):
|
def setup_users_from_config(client: GiteaAPIClient, wf: dict, mt: dict, stats: dict = None, handled: dict = None):
|
||||||
"""
|
"""
|
||||||
Parses workflow.config and _maintainership.json, creates users, and adds them as collaborators.
|
Parses workflow.config and _maintainership.json, creates users, and adds them as collaborators.
|
||||||
"""
|
"""
|
||||||
@@ -101,13 +196,19 @@ def setup_users_from_config(client: GiteaAPIClient, wf: dict, mt: dict):
|
|||||||
|
|
||||||
# Create all users
|
# Create all users
|
||||||
for username in all_users:
|
for username in all_users:
|
||||||
if username not in _CREATED_USERS:
|
new_user = client.create_user(username, "password123", f"{username}@example.com")
|
||||||
client.create_user(username, "password123", f"{username}@example.com")
|
_CREATED_USERS.add(username)
|
||||||
_CREATED_USERS.add(username)
|
if stats and handled and username not in handled["users"]:
|
||||||
|
handled["users"].add(username)
|
||||||
|
if new_user: stats["users"]["new"] += 1
|
||||||
|
else: stats["users"]["reused"] += 1
|
||||||
|
|
||||||
if ("myproducts/mySLFO", username) not in _ADDED_COLLABORATORS:
|
new_coll = client.add_collaborator("myproducts", "mySLFO", username, "write")
|
||||||
client.add_collaborator("myproducts", "mySLFO", username, "write")
|
_ADDED_COLLABORATORS.add(("myproducts/mySLFO", username))
|
||||||
_ADDED_COLLABORATORS.add(("myproducts/mySLFO", username))
|
if stats and handled and ("myproducts/mySLFO", username) not in handled["collaborators"]:
|
||||||
|
handled["collaborators"].add(("myproducts/mySLFO", username))
|
||||||
|
if new_coll: stats["collaborators"]["new"] += 1
|
||||||
|
else: stats["collaborators"]["reused"] += 1
|
||||||
|
|
||||||
# Set specific repository permissions based on maintainership
|
# Set specific repository permissions based on maintainership
|
||||||
for pkg, users in mt.items():
|
for pkg, users in mt.items():
|
||||||
@@ -115,20 +216,34 @@ def setup_users_from_config(client: GiteaAPIClient, wf: dict, mt: dict):
|
|||||||
for username in users:
|
for username in users:
|
||||||
if not repo_name:
|
if not repo_name:
|
||||||
for r in ["pkgA", "pkgB"]:
|
for r in ["pkgA", "pkgB"]:
|
||||||
if (f"mypool/{r}", username) not in _ADDED_COLLABORATORS:
|
new_coll = client.add_collaborator("mypool", r, username, "write")
|
||||||
client.add_collaborator("mypool", r, username, "write")
|
_ADDED_COLLABORATORS.add((f"mypool/{r}", username))
|
||||||
_ADDED_COLLABORATORS.add((f"mypool/{r}", username))
|
if stats and handled and (f"mypool/{r}", username) not in handled["collaborators"]:
|
||||||
|
handled["collaborators"].add((f"mypool/{r}", username))
|
||||||
|
if new_coll: stats["collaborators"]["new"] += 1
|
||||||
|
else: stats["collaborators"]["reused"] += 1
|
||||||
else:
|
else:
|
||||||
if (f"mypool/{repo_name}", username) not in _ADDED_COLLABORATORS:
|
new_coll = client.add_collaborator("mypool", repo_name, username, "write")
|
||||||
client.add_collaborator("mypool", repo_name, username, "write")
|
_ADDED_COLLABORATORS.add((f"mypool/{repo_name}", username))
|
||||||
_ADDED_COLLABORATORS.add((f"mypool/{repo_name}", username))
|
if stats and handled and (f"mypool/{repo_name}", username) not in handled["collaborators"]:
|
||||||
|
handled["collaborators"].add((f"mypool/{repo_name}", username))
|
||||||
|
if new_coll: stats["collaborators"]["new"] += 1
|
||||||
|
else: stats["collaborators"]["reused"] += 1
|
||||||
|
|
||||||
def ensure_config_file(client: GiteaAPIClient, owner: str, repo: str, branch: str, file_name: str, expected_content_dict: dict):
|
def ensure_config_file(client: GiteaAPIClient, owner: str, repo: str, branch: str, file_name: str, expected_content_dict: dict, existing_files: list = None):
|
||||||
"""
|
"""
|
||||||
Checks if a config file exists and has the correct content.
|
Checks if a config file exists and has the correct content.
|
||||||
Returns True if a change was made, False otherwise.
|
Returns True if a change was made, False otherwise.
|
||||||
"""
|
"""
|
||||||
file_info = client.get_file_info(owner, repo, file_name, branch=branch)
|
file_info = None
|
||||||
|
if existing_files is not None:
|
||||||
|
if file_name not in [f["path"] for f in existing_files]:
|
||||||
|
pass # File definitely doesn't exist
|
||||||
|
else:
|
||||||
|
file_info = client.get_file_info(owner, repo, file_name, branch=branch)
|
||||||
|
else:
|
||||||
|
file_info = client.get_file_info(owner, repo, file_name, branch=branch)
|
||||||
|
|
||||||
expected_content = json.dumps(expected_content_dict, indent=4)
|
expected_content = json.dumps(expected_content_dict, indent=4)
|
||||||
|
|
||||||
if file_info:
|
if file_info:
|
||||||
@@ -148,8 +263,28 @@ def gitea_env():
|
|||||||
"""
|
"""
|
||||||
Global fixture to set up the Gitea environment for all tests.
|
Global fixture to set up the Gitea environment for all tests.
|
||||||
"""
|
"""
|
||||||
gitea_url = "http://127.0.0.1:3000"
|
setup_start_time = time.time()
|
||||||
admin_token_path = "./gitea-data/admin.token"
|
stats = {
|
||||||
|
"orgs": {"new": 0, "reused": 0},
|
||||||
|
"repos": {"new": 0, "reused": 0},
|
||||||
|
"users": {"new": 0, "reused": 0},
|
||||||
|
"labels": {"new": 0, "reused": 0},
|
||||||
|
"collaborators": {"new": 0, "reused": 0},
|
||||||
|
"branches": {"new": 0, "reused": 0},
|
||||||
|
"webhooks": {"new": 0, "reused": 0},
|
||||||
|
}
|
||||||
|
handled_in_session = {
|
||||||
|
"orgs": set(),
|
||||||
|
"repos": set(),
|
||||||
|
"users": set(),
|
||||||
|
"labels": set(),
|
||||||
|
"collaborators": set(),
|
||||||
|
"branches": set(),
|
||||||
|
"webhooks": set(),
|
||||||
|
}
|
||||||
|
|
||||||
|
gitea_url = "http://gitea-test:3000"
|
||||||
|
admin_token_path = os.path.join(os.path.dirname(__file__), "..", "gitea-data", "admin.token")
|
||||||
|
|
||||||
admin_token = None
|
admin_token = None
|
||||||
try:
|
try:
|
||||||
@@ -159,35 +294,55 @@ def gitea_env():
|
|||||||
raise Exception(f"Admin token file not found at {admin_token_path}.")
|
raise Exception(f"Admin token file not found at {admin_token_path}.")
|
||||||
|
|
||||||
client = GiteaAPIClient(base_url=gitea_url, token=admin_token)
|
client = GiteaAPIClient(base_url=gitea_url, token=admin_token)
|
||||||
|
client.use_cache = True
|
||||||
|
|
||||||
# Wait for Gitea
|
# Wait for Gitea
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
try:
|
try:
|
||||||
if client._request("GET", "version").status_code == 200:
|
resp, dur = client._request("GET", "version")
|
||||||
|
if resp.status_code == 200:
|
||||||
|
vprint(f"DEBUG: Gitea connection successful (duration: {dur:.3f}s)")
|
||||||
break
|
break
|
||||||
except:
|
except Exception as e:
|
||||||
|
vprint(f"DEBUG: Gitea connection attempt {i+1} failed: {e}")
|
||||||
pass
|
pass
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
else:
|
else: raise Exception("Gitea not available.")
|
||||||
raise Exception("Gitea not available.")
|
|
||||||
|
|
||||||
print("--- Starting Gitea Global Setup ---")
|
vprint("--- Starting Gitea Global Setup ---")
|
||||||
for org in ["myproducts", "mypool"]:
|
for org in ["myproducts", "mypool"]:
|
||||||
if org not in _CREATED_ORGS:
|
new_org = client.create_org(org)
|
||||||
client.create_org(org)
|
_CREATED_ORGS.add(org)
|
||||||
_CREATED_ORGS.add(org)
|
if org not in handled_in_session["orgs"]:
|
||||||
|
handled_in_session["orgs"].add(org)
|
||||||
|
if new_org: stats["orgs"]["new"] += 1
|
||||||
|
else: stats["orgs"]["reused"] += 1
|
||||||
|
|
||||||
for org, repo in [("myproducts", "mySLFO"), ("mypool", "pkgA"), ("mypool", "pkgB")]:
|
for org, repo in [("myproducts", "mySLFO"), ("mypool", "pkgA"), ("mypool", "pkgB")]:
|
||||||
if f"{org}/{repo}" not in _CREATED_REPOS:
|
new_repo = client.create_repo(org, repo)
|
||||||
client.create_repo(org, repo)
|
client.update_repo_settings(org, repo)
|
||||||
client.update_repo_settings(org, repo)
|
repo_full = f"{org}/{repo}"
|
||||||
_CREATED_REPOS.add(f"{org}/{repo}")
|
_CREATED_REPOS.add(repo_full)
|
||||||
|
if repo_full not in handled_in_session["repos"]:
|
||||||
|
handled_in_session["repos"].add(repo_full)
|
||||||
|
if new_repo: stats["repos"]["new"] += 1
|
||||||
|
else: stats["repos"]["reused"] += 1
|
||||||
|
|
||||||
|
# Create webhook for publisher
|
||||||
|
new_hook = client.create_webhook(org, repo, "http://gitea-publisher:8002/rabbitmq-forwarder")
|
||||||
|
if repo_full not in handled_in_session["webhooks"]:
|
||||||
|
handled_in_session["webhooks"].add(repo_full)
|
||||||
|
if new_hook: stats["webhooks"]["new"] += 1
|
||||||
|
else: stats["webhooks"]["reused"] += 1
|
||||||
|
|
||||||
# Create labels
|
# Create labels
|
||||||
for name, color in [("staging/Backlog", "#0000ff"), ("review/Pending", "#ffff00")]:
|
for name, color in [("staging/Backlog", "#0000ff"), ("review/Pending", "#ffff00")]:
|
||||||
if ("myproducts/mySLFO", name) not in _CREATED_LABELS:
|
new_label = client.create_label("myproducts", "mySLFO", name, color=color)
|
||||||
client.create_label("myproducts", "mySLFO", name, color=color)
|
_CREATED_LABELS.add(("myproducts/mySLFO", name))
|
||||||
_CREATED_LABELS.add(("myproducts/mySLFO", name))
|
if ("myproducts/mySLFO", name) not in handled_in_session["labels"]:
|
||||||
|
handled_in_session["labels"].add(("myproducts/mySLFO", name))
|
||||||
|
if new_label: stats["labels"]["new"] += 1
|
||||||
|
else: stats["labels"]["reused"] += 1
|
||||||
|
|
||||||
# Submodules in mySLFO
|
# Submodules in mySLFO
|
||||||
client.add_submodules("myproducts", "mySLFO")
|
client.add_submodules("myproducts", "mySLFO")
|
||||||
@@ -196,24 +351,51 @@ def gitea_env():
|
|||||||
("myproducts/mySLFO", "workflow-pr"),
|
("myproducts/mySLFO", "workflow-pr"),
|
||||||
("mypool/pkgA", "workflow-pr"),
|
("mypool/pkgA", "workflow-pr"),
|
||||||
("mypool/pkgB", "workflow-pr")]:
|
("mypool/pkgB", "workflow-pr")]:
|
||||||
if (repo_full, bot) not in _ADDED_COLLABORATORS:
|
org_part, repo_part = repo_full.split("/")
|
||||||
org_part, repo_part = repo_full.split("/")
|
new_coll = client.add_collaborator(org_part, repo_part, bot, "write")
|
||||||
client.add_collaborator(org_part, repo_part, bot, "write")
|
_ADDED_COLLABORATORS.add((repo_full, bot))
|
||||||
_ADDED_COLLABORATORS.add((repo_full, bot))
|
if (repo_full, bot) not in handled_in_session["collaborators"]:
|
||||||
|
handled_in_session["collaborators"].add((repo_full, bot))
|
||||||
|
if new_coll: stats["collaborators"]["new"] += 1
|
||||||
|
else: stats["collaborators"]["reused"] += 1
|
||||||
|
|
||||||
|
# Collect all users from all configurations first to do setup once
|
||||||
|
all_setup_users_wf = {}
|
||||||
|
all_setup_users_mt = {}
|
||||||
|
|
||||||
restart_needed = False
|
|
||||||
|
|
||||||
# Setup all branches and configs
|
# Setup all branches and configs
|
||||||
|
repo_list = [("mypool", "pkgA"), ("mypool", "pkgB"), ("myproducts", "mySLFO")]
|
||||||
|
repo_branches = {}
|
||||||
|
for owner, repo in repo_list:
|
||||||
|
resp, _ = client._request("GET", f"repos/{owner}/{repo}/branches")
|
||||||
|
repo_branches[(owner, repo)] = {b["name"] for b in resp.json()}
|
||||||
|
|
||||||
for branch_name, custom_configs in BRANCH_CONFIG_CUSTOM.items():
|
for branch_name, custom_configs in BRANCH_CONFIG_CUSTOM.items():
|
||||||
# Ensure branch exists in all 3 repos
|
# Ensure branch exists in all 3 repos
|
||||||
for owner, repo in [("myproducts", "mySLFO"), ("mypool", "pkgA"), ("mypool", "pkgB")]:
|
for owner, repo in repo_list:
|
||||||
if branch_name != "main":
|
if branch_name != "main":
|
||||||
try:
|
if branch_name not in repo_branches[(owner, repo)]:
|
||||||
main_sha = client._request("GET", f"repos/{owner}/{repo}/branches/main").json()["commit"]["id"]
|
try:
|
||||||
client.create_branch(owner, repo, branch_name, main_sha)
|
resp, _ = client._request("GET", f"repos/{owner}/{repo}/branches/main")
|
||||||
except Exception as e:
|
main_sha = resp.json()["commit"]["id"]
|
||||||
if "already exists" not in str(e).lower():
|
new_branch = client.create_branch(owner, repo, branch_name, main_sha)
|
||||||
raise
|
repo_branches[(owner, repo)].add(branch_name)
|
||||||
|
if (f"{owner}/{repo}", branch_name) not in handled_in_session["branches"]:
|
||||||
|
handled_in_session["branches"].add((f"{owner}/{repo}", branch_name))
|
||||||
|
if new_branch: stats["branches"]["new"] += 1
|
||||||
|
else: stats["branches"]["reused"] += 1
|
||||||
|
except Exception as e:
|
||||||
|
if "already exists" not in str(e).lower():
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
if (f"{owner}/{repo}", branch_name) not in handled_in_session["branches"]:
|
||||||
|
handled_in_session["branches"].add((f"{owner}/{repo}", branch_name))
|
||||||
|
stats["branches"]["reused"] += 1
|
||||||
|
else:
|
||||||
|
# main branch always exists, but let's track it as reused if not handled
|
||||||
|
if (f"{owner}/{repo}", "main") not in handled_in_session["branches"]:
|
||||||
|
handled_in_session["branches"].add((f"{owner}/{repo}", "main"))
|
||||||
|
stats["branches"]["reused"] += 1
|
||||||
|
|
||||||
# Merge configs
|
# Merge configs
|
||||||
merged_configs = {}
|
merged_configs = {}
|
||||||
@@ -232,19 +414,40 @@ def gitea_env():
|
|||||||
else:
|
else:
|
||||||
merged_configs[file_name] = custom_content
|
merged_configs[file_name] = custom_content
|
||||||
|
|
||||||
|
# Pre-fetch existing files in this branch to avoid 404s in ensure_config_file
|
||||||
|
try:
|
||||||
|
resp, _ = client._request("GET", f"repos/myproducts/mySLFO/contents?ref={branch_name}")
|
||||||
|
existing_files = resp.json()
|
||||||
|
except:
|
||||||
|
existing_files = []
|
||||||
|
|
||||||
# Ensure config files in myproducts/mySLFO
|
# Ensure config files in myproducts/mySLFO
|
||||||
for file_name, content_dict in merged_configs.items():
|
for file_name, content_dict in merged_configs.items():
|
||||||
if ensure_config_file(client, "myproducts", "mySLFO", branch_name, file_name, content_dict):
|
ensure_config_file(client, "myproducts", "mySLFO", branch_name, file_name, content_dict, existing_files=existing_files)
|
||||||
restart_needed = True
|
|
||||||
|
|
||||||
# Setup users (using configs from this branch)
|
# Collect configs for user setup
|
||||||
setup_users_from_config(client, merged_configs.get("workflow.config", {}), merged_configs.get("_maintainership.json", {}))
|
wf_cfg = merged_configs.get("workflow.config", {})
|
||||||
|
mt_cfg = merged_configs.get("_maintainership.json", {})
|
||||||
|
# Simple merge for user collection
|
||||||
|
if "Reviewers" in wf_cfg:
|
||||||
|
all_setup_users_wf.setdefault("Reviewers", []).extend(wf_cfg["Reviewers"])
|
||||||
|
for k, v in mt_cfg.items():
|
||||||
|
all_setup_users_mt.setdefault(k, []).extend(v)
|
||||||
|
|
||||||
if restart_needed:
|
# Dedup and setup users once
|
||||||
client.restart_service("workflow-pr")
|
if "Reviewers" in all_setup_users_wf:
|
||||||
time.sleep(2) # Give it time to pick up changes
|
all_setup_users_wf["Reviewers"] = list(set(all_setup_users_wf["Reviewers"]))
|
||||||
|
for k in all_setup_users_mt:
|
||||||
|
all_setup_users_mt[k] = list(set(all_setup_users_mt[k]))
|
||||||
|
|
||||||
|
setup_users_from_config(client, all_setup_users_wf, all_setup_users_mt, stats=stats, handled=handled_in_session)
|
||||||
|
|
||||||
print("--- Gitea Global Setup Complete ---")
|
setup_duration = time.time() - setup_start_time
|
||||||
|
print(f"--- Gitea Global Setup Complete (took {setup_duration:.2f}s) ---\n"
|
||||||
|
f"Objects created: {stats['orgs']['new']} orgs, {stats['repos']['new']} repos, {stats['branches']['new']} branches, {stats['webhooks']['new']} webhooks, {stats['users']['new']} users, {stats['labels']['new']} labels, {stats['collaborators']['new']} collaborators\n"
|
||||||
|
f"Objects reused: {stats['orgs']['reused']} orgs, {stats['repos']['reused']} repos, {stats['branches']['reused']} branches, {stats['webhooks']['reused']} webhooks, {stats['users']['reused']} users, {stats['labels']['reused']} labels, {stats['collaborators']['reused']} collaborators")
|
||||||
|
|
||||||
|
client.use_cache = False
|
||||||
yield client
|
yield client
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
@@ -275,6 +478,18 @@ def no_project_git_pr_env(gitea_env):
|
|||||||
def label_env(gitea_env):
|
def label_env(gitea_env):
|
||||||
return gitea_env, "myproducts/mySLFO", "label-test"
|
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")
|
@pytest.fixture(scope="session")
|
||||||
def usera_client(gitea_env):
|
def usera_client(gitea_env):
|
||||||
return GiteaAPIClient(base_url=gitea_env.base_url, token=gitea_env.headers["Authorization"].split(" ")[1], sudo="usera")
|
return GiteaAPIClient(base_url=gitea_env.base_url, token=gitea_env.headers["Authorization"].split(" ")[1], sudo="usera")
|
||||||
|
|||||||
@@ -3,45 +3,16 @@ import time
|
|||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import base64
|
import base64
|
||||||
import subprocess
|
|
||||||
|
|
||||||
TEST_DATA_DIR = Path(__file__).parent.parent / "data"
|
IS_TEST_RUN = False
|
||||||
BUILD_RESULT_TEMPLATE = TEST_DATA_DIR / "build_result.xml.template"
|
|
||||||
MOCK_RESPONSES_DIR = Path(__file__).parent.parent.parent / "mock-obs" / "responses"
|
|
||||||
MOCK_BUILD_RESULT_FILE = (
|
|
||||||
MOCK_RESPONSES_DIR / "GET_build_openSUSE:Leap:16.0:PullRequest:*__result"
|
|
||||||
)
|
|
||||||
MOCK_BUILD_RESULT_FILE1 = MOCK_RESPONSES_DIR / "GET_build_openSUSE:Leap:16.0__result"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_build_result():
|
|
||||||
"""
|
|
||||||
Fixture to create a mock build result file from the template.
|
|
||||||
Returns a factory function that the test can call with parameters.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _create_result_file(package_name: str, code: str):
|
|
||||||
tree = ET.parse(BUILD_RESULT_TEMPLATE)
|
|
||||||
root = tree.getroot()
|
|
||||||
for status_tag in root.findall(".//status"):
|
|
||||||
status_tag.set("package", package_name)
|
|
||||||
status_tag.set("code", code)
|
|
||||||
|
|
||||||
MOCK_RESPONSES_DIR.mkdir(exist_ok=True)
|
|
||||||
tree.write(MOCK_BUILD_RESULT_FILE)
|
|
||||||
tree.write(MOCK_BUILD_RESULT_FILE1)
|
|
||||||
return str(MOCK_BUILD_RESULT_FILE)
|
|
||||||
|
|
||||||
yield _create_result_file
|
|
||||||
|
|
||||||
if MOCK_BUILD_RESULT_FILE.exists():
|
|
||||||
MOCK_BUILD_RESULT_FILE.unlink()
|
|
||||||
MOCK_BUILD_RESULT_FILE1.unlink()
|
|
||||||
|
|
||||||
|
def vprint(*args, **kwargs):
|
||||||
|
if IS_TEST_RUN or os.environ.get("AUTOGITS_PRINT_FIXTURES") == "1":
|
||||||
|
print(*args, **kwargs)
|
||||||
|
|
||||||
class GiteaAPIClient:
|
class GiteaAPIClient:
|
||||||
def __init__(self, base_url, token, sudo=None):
|
def __init__(self, base_url, token, sudo=None):
|
||||||
@@ -49,24 +20,45 @@ class GiteaAPIClient:
|
|||||||
self.headers = {"Authorization": f"token {token}", "Content-Type": "application/json"}
|
self.headers = {"Authorization": f"token {token}", "Content-Type": "application/json"}
|
||||||
if sudo:
|
if sudo:
|
||||||
self.headers["Sudo"] = sudo
|
self.headers["Sudo"] = sudo
|
||||||
|
self._cache = {}
|
||||||
|
self.use_cache = False
|
||||||
|
|
||||||
def _request(self, method, path, **kwargs):
|
def _request(self, method, path, **kwargs):
|
||||||
|
# Very basic cache for GET requests to speed up setup
|
||||||
|
cache_key = (method, path, json.dumps(kwargs, sort_keys=True))
|
||||||
|
if self.use_cache and method == "GET" and cache_key in self._cache:
|
||||||
|
return self._cache[cache_key], 0.0
|
||||||
|
|
||||||
url = f"{self.base_url}/api/v1/{path}"
|
url = f"{self.base_url}/api/v1/{path}"
|
||||||
response = requests.request(method, url, headers=self.headers, **kwargs)
|
start_time = time.time()
|
||||||
try:
|
try:
|
||||||
|
response = requests.request(method, url, headers=self.headers, **kwargs)
|
||||||
|
duration = time.time() - start_time
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
|
if self.use_cache:
|
||||||
|
if method == "GET":
|
||||||
|
self._cache[cache_key] = response
|
||||||
|
else:
|
||||||
|
self._cache.clear()
|
||||||
|
|
||||||
|
return response, duration
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
print(f"HTTPError in _request: {e}")
|
duration = time.time() - start_time
|
||||||
print(f"Response Content: {e.response.text}")
|
vprint(f"[{duration:.3f}s] HTTPError in _request: {e}")
|
||||||
|
vprint(f"Response Content: {e.response.text}")
|
||||||
|
raise
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
duration = time.time() - start_time
|
||||||
|
vprint(f"[{duration:.3f}s] Request failed: {e}")
|
||||||
raise
|
raise
|
||||||
return response
|
|
||||||
|
|
||||||
def get_file_info(self, owner: str, repo: str, file_path: str, branch: str = "main"):
|
def get_file_info(self, owner: str, repo: str, file_path: str, branch: str = "main"):
|
||||||
url = f"repos/{owner}/{repo}/contents/{file_path}"
|
url = f"repos/{owner}/{repo}/contents/{file_path}"
|
||||||
if branch and branch != "main":
|
if branch and branch != "main":
|
||||||
url += f"?ref={branch}"
|
url += f"?ref={branch}"
|
||||||
try:
|
try:
|
||||||
response = self._request("GET", url)
|
response, duration = self._request("GET", url)
|
||||||
return response.json()
|
return response.json()
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
if e.response.status_code == 404:
|
if e.response.status_code == 404:
|
||||||
@@ -74,7 +66,7 @@ class GiteaAPIClient:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
def create_user(self, username, password, email):
|
def create_user(self, username, password, email):
|
||||||
print(f"--- Creating user: {username} ---")
|
vprint(f"--- Creating user: {username} ---")
|
||||||
data = {
|
data = {
|
||||||
"username": username,
|
"username": username,
|
||||||
"password": password,
|
"password": password,
|
||||||
@@ -83,18 +75,20 @@ class GiteaAPIClient:
|
|||||||
"send_notify": False
|
"send_notify": False
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
self._request("POST", "admin/users", json=data)
|
response, duration = self._request("POST", "admin/users", json=data)
|
||||||
print(f"User '{username}' created.")
|
vprint(f"[{duration:.3f}s] User '{username}' created.")
|
||||||
|
return True
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
if e.response.status_code == 422: # Already exists
|
if e.response.status_code == 422: # Already exists
|
||||||
print(f"User '{username}' already exists. Updating password...")
|
vprint(f"User '{username}' already exists. Updating password...")
|
||||||
# Update password to be sure it matches our expectation
|
# Update password to be sure it matches our expectation
|
||||||
self._request("PATCH", f"admin/users/{username}", json={"password": password, "login_name": username})
|
response, duration = self._request("PATCH", f"admin/users/{username}", json={"password": password, "login_name": username})
|
||||||
|
return False
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def get_user_token(self, username, password, token_name="test-token"):
|
def get_user_token(self, username, password, token_name="test-token"):
|
||||||
print(f"--- Getting token for user: {username} ---")
|
vprint(f"--- Getting token for user: {username} ---")
|
||||||
url = f"{self.base_url}/api/v1/users/{username}/tokens"
|
url = f"{self.base_url}/api/v1/users/{username}/tokens"
|
||||||
|
|
||||||
# Create new token using Basic Auth
|
# Create new token using Basic Auth
|
||||||
@@ -104,39 +98,30 @@ class GiteaAPIClient:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
def create_org(self, org_name):
|
def create_org(self, org_name):
|
||||||
print(f"--- Checking organization: {org_name} ---")
|
vprint(f"--- Checking organization: {org_name} ---")
|
||||||
try:
|
try:
|
||||||
self._request("GET", f"orgs/{org_name}")
|
response, duration = self._request("GET", f"orgs/{org_name}")
|
||||||
print(f"Organization '{org_name}' already exists.")
|
vprint(f"[{duration:.3f}s] Organization '{org_name}' already exists.")
|
||||||
|
return False
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
if e.response.status_code == 404:
|
if e.response.status_code == 404:
|
||||||
print(f"Creating organization '{org_name}'...")
|
vprint(f"Creating organization '{org_name}'...")
|
||||||
data = {"username": org_name, "full_name": org_name}
|
data = {"username": org_name, "full_name": org_name}
|
||||||
self._request("POST", "orgs", json=data)
|
response, duration = self._request("POST", "orgs", json=data)
|
||||||
print(f"Organization '{org_name}' created.")
|
vprint(f"[{duration:.3f}s] Organization '{org_name}' created.")
|
||||||
else:
|
return True
|
||||||
raise
|
|
||||||
print(f"--- Checking organization: {org_name} ---")
|
|
||||||
try:
|
|
||||||
self._request("GET", f"orgs/{org_name}")
|
|
||||||
print(f"Organization '{org_name}' already exists.")
|
|
||||||
except requests.exceptions.HTTPError as e:
|
|
||||||
if e.response.status_code == 404:
|
|
||||||
print(f"Creating organization '{org_name}'...")
|
|
||||||
data = {"username": org_name, "full_name": org_name}
|
|
||||||
self._request("POST", "orgs", json=data)
|
|
||||||
print(f"Organization '{org_name}' created.")
|
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def create_repo(self, org_name, repo_name):
|
def create_repo(self, org_name, repo_name):
|
||||||
print(f"--- Checking repository: {org_name}/{repo_name} ---")
|
vprint(f"--- Checking repository: {org_name}/{repo_name} ---")
|
||||||
try:
|
try:
|
||||||
self._request("GET", f"repos/{org_name}/{repo_name}")
|
response, duration = self._request("GET", f"repos/{org_name}/{repo_name}")
|
||||||
print(f"Repository '{org_name}/{repo_name}' already exists.")
|
vprint(f"[{duration:.3f}s] Repository '{org_name}/{repo_name}' already exists.")
|
||||||
|
return False
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
if e.response.status_code == 404:
|
if e.response.status_code == 404:
|
||||||
print(f"Creating repository '{org_name}/{repo_name}'...")
|
vprint(f"Creating repository '{org_name}/{repo_name}'...")
|
||||||
data = {
|
data = {
|
||||||
"name": repo_name,
|
"name": repo_name,
|
||||||
"auto_init": True,
|
"auto_init": True,
|
||||||
@@ -144,43 +129,58 @@ class GiteaAPIClient:
|
|||||||
"gitignores": "Go",
|
"gitignores": "Go",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": False,
|
"private": False,
|
||||||
"readme": "Default"
|
"readme": "Default",
|
||||||
|
"object_format_name": "sha256"
|
||||||
}
|
}
|
||||||
self._request("POST", f"orgs/{org_name}/repos", json=data)
|
response, duration = self._request("POST", f"orgs/{org_name}/repos", json=data)
|
||||||
print(f"Repository '{org_name}/{repo_name}' created with a README.")
|
vprint(f"[{duration:.3f}s] Repository '{org_name}/{repo_name}' created with a README.")
|
||||||
time.sleep(0.1) # Added delay to allow Git operations to become available
|
time.sleep(0.1) # Added delay to allow Git operations to become available
|
||||||
|
return True
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def add_collaborator(self, org_name, repo_name, collaborator_name, permission="write"):
|
def add_collaborator(self, org_name, repo_name, collaborator_name, permission="write"):
|
||||||
print(f"--- Adding {collaborator_name} as a collaborator to {org_name}/{repo_name} with '{permission}' permission ---")
|
vprint(f"--- Adding {collaborator_name} as a collaborator to {org_name}/{repo_name} with '{permission}' permission ---")
|
||||||
|
|
||||||
|
# Check if already a collaborator to provide accurate stats
|
||||||
|
try:
|
||||||
|
self._request("GET", f"repos/{org_name}/{repo_name}/collaborators/{collaborator_name}")
|
||||||
|
vprint(f"{collaborator_name} is already a collaborator of {org_name}/{repo_name}.")
|
||||||
|
return False
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
if e.response.status_code != 404:
|
||||||
|
raise
|
||||||
|
|
||||||
data = {"permission": permission}
|
data = {"permission": permission}
|
||||||
# Gitea API returns 204 No Content on success and doesn't fail if already present.
|
# Gitea API returns 204 No Content on success and doesn't fail if already present.
|
||||||
self._request("PUT", f"repos/{org_name}/{repo_name}/collaborators/{collaborator_name}", json=data)
|
response, duration = self._request("PUT", f"repos/{org_name}/{repo_name}/collaborators/{collaborator_name}", json=data)
|
||||||
print(f"Attempted to add {collaborator_name} to {org_name}/{repo_name}.")
|
vprint(f"[{duration:.3f}s] Added {collaborator_name} to {org_name}/{repo_name}.")
|
||||||
|
return True
|
||||||
|
|
||||||
def add_submodules(self, org_name, repo_name):
|
def add_submodules(self, org_name, repo_name):
|
||||||
print(f"--- Adding submodules to {org_name}/{repo_name} using diffpatch ---")
|
vprint(f"--- Adding submodules to {org_name}/{repo_name} using diffpatch ---")
|
||||||
parent_repo_path = f"repos/{org_name}/{repo_name}"
|
parent_repo_path = f"repos/{org_name}/{repo_name}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._request("GET", f"{parent_repo_path}/contents/.gitmodules")
|
response, duration = self._request("GET", f"{parent_repo_path}/contents/.gitmodules")
|
||||||
print("Submodules appear to be already added. Skipping.")
|
vprint(f"[{duration:.3f}s] Submodules appear to be already added. Skipping.")
|
||||||
return
|
return
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
if e.response.status_code != 404:
|
if e.response.status_code != 404:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# Get latest commit SHAs for the submodules
|
# Get latest commit SHAs for the submodules
|
||||||
pkg_a_sha = self._request("GET", "repos/mypool/pkgA/branches/main").json()["commit"]["id"]
|
response_a, duration_a = self._request("GET", "repos/mypool/pkgA/branches/main")
|
||||||
pkg_b_sha = self._request("GET", "repos/mypool/pkgB/branches/main").json()["commit"]["id"]
|
pkg_a_sha = response_a.json()["commit"]["id"]
|
||||||
|
response_b, duration_b = self._request("GET", "repos/mypool/pkgB/branches/main")
|
||||||
|
pkg_b_sha = response_b.json()["commit"]["id"]
|
||||||
|
|
||||||
if not pkg_a_sha or not pkg_b_sha:
|
if not pkg_a_sha or not pkg_b_sha:
|
||||||
raise Exception("Error: Could not get submodule commit SHAs. Cannot apply patch.")
|
raise Exception("Error: Could not get submodule commit SHAs. Cannot apply patch.")
|
||||||
|
|
||||||
diff_content = f"""diff --git a/.gitmodules b/.gitmodules
|
diff_content = f"""diff --git a/.gitmodules b/.gitmodules
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000..f1838bd
|
index 00000000..f1838bd9
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
+++ b/.gitmodules
|
+++ b/.gitmodules
|
||||||
@@ -0,0 +1,6 @@
|
@@ -0,0 +1,6 @@
|
||||||
@@ -192,14 +192,14 @@ index 0000000..f1838bd
|
|||||||
+ url = ../../mypool/pkgB.git
|
+ url = ../../mypool/pkgB.git
|
||||||
diff --git a/pkgA b/pkgA
|
diff --git a/pkgA b/pkgA
|
||||||
new file mode 160000
|
new file mode 160000
|
||||||
index 0000000..{pkg_a_sha}
|
index 00000000..{pkg_a_sha}
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
+++ b/pkgA
|
+++ b/pkgA
|
||||||
@@ -0,0 +1 @@
|
@@ -0,0 +1 @@
|
||||||
+Subproject commit {pkg_a_sha}
|
+Subproject commit {pkg_a_sha}
|
||||||
diff --git a/pkgB b/pkgB
|
diff --git a/pkgB b/pkgB
|
||||||
new file mode 160000
|
new file mode 160000
|
||||||
index 0000000..{pkg_b_sha}
|
index 00000000..{pkg_b_sha}
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
+++ b/pkgB
|
+++ b/pkgB
|
||||||
@@ -0,0 +1 @@
|
@@ -0,0 +1 @@
|
||||||
@@ -211,34 +211,81 @@ index 0000000..{pkg_b_sha}
|
|||||||
"content": diff_content,
|
"content": diff_content,
|
||||||
"message": message
|
"message": message
|
||||||
}
|
}
|
||||||
print(f"Applying submodule patch to {org_name}/{repo_name}...")
|
vprint(f"Applying submodule patch to {org_name}/{repo_name}...")
|
||||||
self._request("POST", f"{parent_repo_path}/diffpatch", json=data)
|
response, duration = self._request("POST", f"{parent_repo_path}/diffpatch", json=data)
|
||||||
print("Submodule patch applied.")
|
vprint(f"[{duration:.3f}s] Submodule patch applied.")
|
||||||
|
|
||||||
def update_repo_settings(self, org_name, repo_name):
|
def update_repo_settings(self, org_name, repo_name):
|
||||||
print(f"--- Updating repository settings for: {org_name}/{repo_name} ---")
|
vprint(f"--- Updating repository settings for: {org_name}/{repo_name} ---")
|
||||||
repo_data = self._request("GET", f"repos/{org_name}/{repo_name}").json()
|
response, duration = self._request("GET", f"repos/{org_name}/{repo_name}")
|
||||||
|
repo_data = response.json()
|
||||||
|
|
||||||
# Ensure these are boolean values, not string
|
# Ensure these are boolean values, not string
|
||||||
repo_data["allow_manual_merge"] = True
|
repo_data["allow_manual_merge"] = True
|
||||||
repo_data["autodetect_manual_merge"] = True
|
repo_data["autodetect_manual_merge"] = True
|
||||||
|
|
||||||
self._request("PATCH", f"repos/{org_name}/{repo_name}", json=repo_data)
|
response, duration = self._request("PATCH", f"repos/{org_name}/{repo_name}", json=repo_data)
|
||||||
print(f"Repository settings for '{org_name}/{repo_name}' updated.")
|
vprint(f"[{duration:.3f}s] Repository settings for '{org_name}/{repo_name}' updated.")
|
||||||
|
|
||||||
|
def create_webhook(self, owner: str, repo: str, target_url: str):
|
||||||
|
vprint(f"--- Checking webhook for {owner}/{repo} -> {target_url} ---")
|
||||||
|
url = f"repos/{owner}/{repo}/hooks"
|
||||||
|
|
||||||
|
try:
|
||||||
|
response, duration = self._request("GET", url)
|
||||||
|
hooks = response.json()
|
||||||
|
for hook in hooks:
|
||||||
|
if hook["config"]["url"] == target_url:
|
||||||
|
vprint(f"Webhook for {owner}/{repo} already exists with correct URL.")
|
||||||
|
return False
|
||||||
|
elif "gitea-publisher" in hook["config"]["url"] or "10.89.0." in hook["config"]["url"]:
|
||||||
|
vprint(f"Found old webhook {hook['id']} with URL {hook['config']['url']}. Deleting...")
|
||||||
|
self._request("DELETE", f"{url}/{hook['id']}")
|
||||||
|
except requests.exceptions.HTTPError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
vprint(f"--- Creating webhook for {owner}/{repo} -> {target_url} ---")
|
||||||
|
data = {
|
||||||
|
"type": "gitea",
|
||||||
|
"config": {
|
||||||
|
"url": target_url,
|
||||||
|
"content_type": "json"
|
||||||
|
},
|
||||||
|
"events": ["push", "pull_request", "pull_request_review", "issue_comment"],
|
||||||
|
"active": True
|
||||||
|
}
|
||||||
|
response, duration = self._request("POST", url, json=data)
|
||||||
|
vprint(f"[{duration:.3f}s] Webhook created for {owner}/{repo}.")
|
||||||
|
return True
|
||||||
|
|
||||||
def create_label(self, owner: str, repo: str, name: str, color: str = "#abcdef"):
|
def create_label(self, owner: str, repo: str, name: str, color: str = "#abcdef"):
|
||||||
print(f"--- Creating label '{name}' in {owner}/{repo} ---")
|
vprint(f"--- Checking label '{name}' in {owner}/{repo} ---")
|
||||||
url = f"repos/{owner}/{repo}/labels"
|
url = f"repos/{owner}/{repo}/labels"
|
||||||
|
|
||||||
|
# Check if label exists first
|
||||||
|
try:
|
||||||
|
response, duration = self._request("GET", url)
|
||||||
|
labels = response.json()
|
||||||
|
for label in labels:
|
||||||
|
if label["name"] == name:
|
||||||
|
vprint(f"Label '{name}' already exists in {owner}/{repo}.")
|
||||||
|
return False
|
||||||
|
except requests.exceptions.HTTPError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
vprint(f"--- Creating label '{name}' in {owner}/{repo} ---")
|
||||||
data = {
|
data = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"color": color
|
"color": color
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
self._request("POST", url, json=data)
|
response, duration = self._request("POST", url, json=data)
|
||||||
print(f"Label '{name}' created.")
|
vprint(f"[{duration:.3f}s] Label '{name}' created.")
|
||||||
|
return True
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
if e.response.status_code == 422: # Already exists
|
if e.response.status_code == 422: # Already exists (race condition or other reason)
|
||||||
print(f"Label '{name}' already exists.")
|
vprint(f"Label '{name}' already exists.")
|
||||||
|
return False
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@@ -252,7 +299,7 @@ index 0000000..{pkg_b_sha}
|
|||||||
}
|
}
|
||||||
|
|
||||||
if file_info:
|
if file_info:
|
||||||
print(f"--- Updating file {file_path} in {owner}/{repo} ---")
|
vprint(f"--- Updating file {file_path} in {owner}/{repo} ---")
|
||||||
# Re-fetch file_info to get the latest SHA right before update
|
# Re-fetch file_info to get the latest SHA right before update
|
||||||
latest_file_info = self.get_file_info(owner, repo, file_path, branch=branch)
|
latest_file_info = self.get_file_info(owner, repo, file_path, branch=branch)
|
||||||
if not latest_file_info:
|
if not latest_file_info:
|
||||||
@@ -261,58 +308,40 @@ index 0000000..{pkg_b_sha}
|
|||||||
data["message"] = f"Update {file_path}"
|
data["message"] = f"Update {file_path}"
|
||||||
method = "PUT"
|
method = "PUT"
|
||||||
else:
|
else:
|
||||||
print(f"--- Creating file {file_path} in {owner}/{repo} ---")
|
vprint(f"--- Creating file {file_path} in {owner}/{repo} ---")
|
||||||
method = "POST"
|
method = "POST"
|
||||||
|
|
||||||
url = f"repos/{owner}/{repo}/contents/{file_path}"
|
url = f"repos/{owner}/{repo}/contents/{file_path}"
|
||||||
self._request(method, url, json=data)
|
response, duration = self._request(method, url, json=data)
|
||||||
print(f"File {file_path} {'updated' if file_info else 'created'} in {owner}/{repo}.")
|
vprint(f"[{duration:.3f}s] File {file_path} {'updated' if file_info else 'created'} in {owner}/{repo}.")
|
||||||
|
|
||||||
def create_gitea_pr(self, repo_full_name: str, diff_content: str, title: str, use_fork: bool, base_branch: str = "main", body: str = ""):
|
def create_gitea_pr(self, repo_full_name: str, diff_content: str, title: str, use_fork: bool, base_branch: str = "main", body: str = ""):
|
||||||
owner, repo = repo_full_name.split("/")
|
owner, repo = repo_full_name.split("/")
|
||||||
|
|
||||||
head_owner, head_repo = owner, repo
|
head_owner, head_repo = owner, repo
|
||||||
|
new_branch_name = f"pr-branch-{int(time.time()*1000)}"
|
||||||
|
|
||||||
if use_fork:
|
if use_fork:
|
||||||
sudo_user = self.headers.get("Sudo")
|
sudo_user = self.headers.get("Sudo")
|
||||||
head_owner = sudo_user
|
head_owner = sudo_user
|
||||||
head_repo = repo
|
head_repo = repo
|
||||||
new_branch_name = f"pr-branch-{int(time.time()*1000)}"
|
|
||||||
|
|
||||||
print(f"--- Forking {repo_full_name} ---")
|
vprint(f"--- Forking {repo_full_name} ---")
|
||||||
try:
|
try:
|
||||||
self._request("POST", f"repos/{owner}/{repo}/forks", json={})
|
response, duration = self._request("POST", f"repos/{owner}/{repo}/forks", json={})
|
||||||
print(f"--- Forked to {head_owner}/{head_repo} ---")
|
vprint(f"[{duration:.3f}s] --- Forked to {head_owner}/{head_repo} ---")
|
||||||
time.sleep(0.5) # Give more time for fork to be ready
|
time.sleep(0.5) # Give more time for fork to be ready
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
if e.response.status_code == 409: # Already forked
|
if e.response.status_code == 409: # Already forked
|
||||||
print(f"--- Already forked to {head_owner}/{head_repo} ---")
|
vprint(f"--- Already forked to {head_owner}/{head_repo} ---")
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# Create a unique branch in the FORK
|
# Apply the diff using diffpatch and create the new branch automatically
|
||||||
base_commit_sha = self._request("GET", f"repos/{owner}/{repo}/branches/{base_branch}").json()["commit"]["id"]
|
vprint(f"--- Applying diff to {head_owner}/{head_repo} from {base_branch} to new branch {new_branch_name} ---")
|
||||||
print(f"--- Creating branch {new_branch_name} in {head_owner}/{head_repo} from {base_branch} ({base_commit_sha}) ---")
|
response, duration = self._request("POST", f"repos/{head_owner}/{head_repo}/diffpatch", json={
|
||||||
self._request("POST", f"repos/{head_owner}/{head_repo}/branches", json={
|
"branch": base_branch,
|
||||||
"new_branch_name": new_branch_name,
|
"new_branch": new_branch_name,
|
||||||
"old_ref": base_commit_sha
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
new_branch_name = f"pr-branch-{int(time.time()*1000)}"
|
|
||||||
# Get the latest commit SHA of the base branch from the ORIGINAL repo
|
|
||||||
base_commit_sha = self._request("GET", f"repos/{owner}/{repo}/branches/{base_branch}").json()["commit"]["id"]
|
|
||||||
|
|
||||||
# Try to create the branch in the ORIGINAL repo
|
|
||||||
print(f"--- Creating branch {new_branch_name} in {repo_full_name} ---")
|
|
||||||
self._request("POST", f"repos/{owner}/{repo}/branches", json={
|
|
||||||
"new_branch_name": new_branch_name,
|
|
||||||
"old_ref": base_commit_sha
|
|
||||||
})
|
|
||||||
|
|
||||||
# Apply the diff using diffpatch in the branch (wherever it is)
|
|
||||||
print(f"--- Applying diff to {head_owner}/{head_repo} branch {new_branch_name} ---")
|
|
||||||
self._request("POST", f"repos/{head_owner}/{head_repo}/diffpatch", json={
|
|
||||||
"branch": new_branch_name,
|
|
||||||
"content": diff_content,
|
"content": diff_content,
|
||||||
"message": title
|
"message": title
|
||||||
})
|
})
|
||||||
@@ -325,59 +354,59 @@ index 0000000..{pkg_b_sha}
|
|||||||
"body": body,
|
"body": body,
|
||||||
"allow_maintainer_edit": True
|
"allow_maintainer_edit": True
|
||||||
}
|
}
|
||||||
print(f"--- Creating PR in {repo_full_name} from {data['head']} ---")
|
vprint(f"--- Creating PR in {repo_full_name} from {data['head']} ---")
|
||||||
response = self._request("POST", f"repos/{owner}/{repo}/pulls", json=data)
|
response, duration = self._request("POST", f"repos/{owner}/{repo}/pulls", json=data)
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
def create_branch(self, owner: str, repo: str, new_branch_name: str, old_ref: str):
|
def create_branch(self, owner: str, repo: str, new_branch_name: str, old_ref: str):
|
||||||
print(f"--- Checking branch '{new_branch_name}' in {owner}/{repo} ---")
|
vprint(f"--- Checking branch '{new_branch_name}' in {owner}/{repo} ---")
|
||||||
try:
|
try:
|
||||||
self._request("GET", f"repos/{owner}/{repo}/branches/{new_branch_name}")
|
response, duration = self._request("GET", f"repos/{owner}/{repo}/branches/{new_branch_name}")
|
||||||
print(f"Branch '{new_branch_name}' already exists.")
|
vprint(f"[{duration:.3f}s] Branch '{new_branch_name}' already exists.")
|
||||||
return
|
return False
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
if e.response.status_code != 404:
|
if e.response.status_code != 404:
|
||||||
raise # Re-raise other HTTP errors
|
raise # Re-raise other HTTP errors
|
||||||
|
|
||||||
print(f"--- Creating branch '{new_branch_name}' in {owner}/{repo} from {old_ref} ---")
|
vprint(f"--- Creating branch '{new_branch_name}' in {owner}/{repo} from {old_ref} ---")
|
||||||
url = f"repos/{owner}/{repo}/branches"
|
url = f"repos/{owner}/{repo}/branches"
|
||||||
data = {
|
data = {
|
||||||
"new_branch_name": new_branch_name,
|
"new_branch_name": new_branch_name,
|
||||||
"old_ref": old_ref
|
"old_ref": old_ref
|
||||||
}
|
}
|
||||||
self._request("POST", url, json=data)
|
response, duration = self._request("POST", url, json=data)
|
||||||
print(f"Branch '{new_branch_name}' created in {owner}/{repo}.")
|
vprint(f"[{duration:.3f}s] Branch '{new_branch_name}' created in {owner}/{repo}.")
|
||||||
|
return True
|
||||||
|
|
||||||
def ensure_branch_exists(self, owner: str, repo: str, branch: str = "main", timeout: int = 10):
|
def ensure_branch_exists(self, owner: str, repo: str, branch: str = "main", timeout: int = 10):
|
||||||
print(f"--- Ensuring branch '{branch}' exists in {owner}/{repo} ---")
|
vprint(f"--- Ensuring branch '{branch}' exists in {owner}/{repo} ---")
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
while time.time() - start_time < timeout:
|
while time.time() - start_time < timeout:
|
||||||
try:
|
try:
|
||||||
self._request("GET", f"repos/{owner}/{repo}/branches/{branch}")
|
response, duration = self._request("GET", f"repos/{owner}/{repo}/branches/{branch}")
|
||||||
print(f"Branch '{branch}' confirmed in {owner}/{repo}.")
|
vprint(f"[{duration:.3f}s] Branch '{branch}' confirmed in {owner}/{repo}.")
|
||||||
return
|
return
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
if e.response.status_code == 404:
|
if e.response.status_code == 404:
|
||||||
print(f"Branch '{branch}' not found yet in {owner}/{repo}. Retrying...")
|
vprint(f"Branch '{branch}' not found yet in {owner}/{repo}. Retrying...")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
continue
|
continue
|
||||||
raise
|
raise
|
||||||
raise Exception(f"Timeout waiting for branch {branch} in {owner}/{repo}")
|
raise Exception(f"Timeout waiting for branch {branch} in {owner}/{repo}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def modify_gitea_pr(self, repo_full_name: str, pr_number: int, diff_content: str, message: str):
|
def modify_gitea_pr(self, repo_full_name: str, pr_number: int, diff_content: str, message: str):
|
||||||
owner, repo = repo_full_name.split("/")
|
owner, repo = repo_full_name.split("/")
|
||||||
|
|
||||||
# Get PR details to find the head branch AND head repo
|
# Get PR details to find the head branch AND head repo
|
||||||
pr_details = self._request("GET", f"repos/{owner}/{repo}/pulls/{pr_number}").json()
|
response, duration = self._request("GET", f"repos/{owner}/{repo}/pulls/{pr_number}")
|
||||||
|
pr_details = response.json()
|
||||||
head_branch = pr_details["head"]["ref"]
|
head_branch = pr_details["head"]["ref"]
|
||||||
head_repo_owner = pr_details["head"]["repo"]["owner"]["login"]
|
head_repo_owner = pr_details["head"]["repo"]["owner"]["login"]
|
||||||
head_repo_name = pr_details["head"]["repo"]["name"]
|
head_repo_name = pr_details["head"]["repo"]["name"]
|
||||||
|
|
||||||
# Apply the diff using diffpatch
|
# Apply the diff using diffpatch
|
||||||
print(f"--- Modifying PR #{pr_number} in {head_repo_owner}/{head_repo_name} branch {head_branch} ---")
|
vprint(f"--- Modifying PR #{pr_number} in {head_repo_owner}/{head_repo_name} branch {head_branch} ---")
|
||||||
self._request("POST", f"repos/{head_repo_owner}/{head_repo_name}/diffpatch", json={
|
response, duration = self._request("POST", f"repos/{head_repo_owner}/{head_repo_name}/diffpatch", json={
|
||||||
"branch": head_branch,
|
"branch": head_branch,
|
||||||
"content": diff_content,
|
"content": diff_content,
|
||||||
"message": message
|
"message": message
|
||||||
@@ -386,15 +415,15 @@ index 0000000..{pkg_b_sha}
|
|||||||
def update_gitea_pr_properties(self, repo_full_name: str, pr_number: int, **kwargs):
|
def update_gitea_pr_properties(self, repo_full_name: str, pr_number: int, **kwargs):
|
||||||
owner, repo = repo_full_name.split("/")
|
owner, repo = repo_full_name.split("/")
|
||||||
url = f"repos/{owner}/{repo}/pulls/{pr_number}"
|
url = f"repos/{owner}/{repo}/pulls/{pr_number}"
|
||||||
response = self._request("PATCH", url, json=kwargs)
|
response, duration = self._request("PATCH", url, json=kwargs)
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
def create_issue_comment(self, repo_full_name: str, issue_number: int, body: str):
|
def create_issue_comment(self, repo_full_name: str, issue_number: int, body: str):
|
||||||
owner, repo = repo_full_name.split("/")
|
owner, repo = repo_full_name.split("/")
|
||||||
url = f"repos/{owner}/{repo}/issues/{issue_number}/comments"
|
url = f"repos/{owner}/{repo}/issues/{issue_number}/comments"
|
||||||
data = {"body": body}
|
data = {"body": body}
|
||||||
print(f"--- Creating comment on {repo_full_name} issue #{issue_number} ---")
|
vprint(f"--- Creating comment on {repo_full_name} issue #{issue_number} ---")
|
||||||
response = self._request("POST", url, json=data)
|
response, duration = self._request("POST", url, json=data)
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
def get_timeline_events(self, repo_full_name: str, pr_number: int):
|
def get_timeline_events(self, repo_full_name: str, pr_number: int):
|
||||||
@@ -404,15 +433,15 @@ index 0000000..{pkg_b_sha}
|
|||||||
# Retry logic for timeline events
|
# Retry logic for timeline events
|
||||||
for i in range(10): # Try up to 10 times
|
for i in range(10): # Try up to 10 times
|
||||||
try:
|
try:
|
||||||
response = self._request("GET", url)
|
response, duration = self._request("GET", url)
|
||||||
timeline_events = response.json()
|
timeline_events = response.json()
|
||||||
if timeline_events: # Check if timeline_events list is not empty
|
if timeline_events: # Check if timeline_events list is not empty
|
||||||
return timeline_events
|
return timeline_events
|
||||||
print(f"Attempt {i+1}: Timeline for PR {pr_number} is empty. Retrying in 1 seconds...")
|
vprint(f"Attempt {i+1}: Timeline for PR {pr_number} is empty. Retrying in 1 seconds...")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
if e.response.status_code == 404:
|
if e.response.status_code == 404:
|
||||||
print(f"Attempt {i+1}: Timeline for PR {pr_number} not found yet. Retrying in 1 seconds...")
|
vprint(f"Attempt {i+1}: Timeline for PR {pr_number} not found yet. Retrying in 1 seconds...")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
else:
|
else:
|
||||||
raise # Re-raise other HTTP errors
|
raise # Re-raise other HTTP errors
|
||||||
@@ -425,16 +454,16 @@ index 0000000..{pkg_b_sha}
|
|||||||
# Retry logic for comments
|
# Retry logic for comments
|
||||||
for i in range(10): # Try up to 10 times
|
for i in range(10): # Try up to 10 times
|
||||||
try:
|
try:
|
||||||
response = self._request("GET", url)
|
response, duration = self._request("GET", url)
|
||||||
comments = response.json()
|
comments = response.json()
|
||||||
print(f"Attempt {i+1}: Comments for PR {pr_number} received: {comments}") # Added debug print
|
vprint(f"[{duration:.3f}s] Attempt {i+1}: Comments for PR {pr_number} received: {comments}") # Added debug print
|
||||||
if comments: # Check if comments list is not empty
|
if comments: # Check if comments list is not empty
|
||||||
return comments
|
return comments
|
||||||
print(f"Attempt {i+1}: Comments for PR {pr_number} are empty. Retrying in 1 seconds...")
|
vprint(f"Attempt {i+1}: Comments for PR {pr_number} are empty. Retrying in 1 seconds...")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
if e.response.status_code == 404:
|
if e.response.status_code == 404:
|
||||||
print(f"Attempt {i+1}: Comments for PR {pr_number} not found yet. Retrying in 1 seconds...")
|
vprint(f"Attempt {i+1}: Comments for PR {pr_number} not found yet. Retrying in 1 seconds...")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
else:
|
else:
|
||||||
raise # Re-raise other HTTP errors
|
raise # Re-raise other HTTP errors
|
||||||
@@ -443,7 +472,7 @@ index 0000000..{pkg_b_sha}
|
|||||||
def get_pr_details(self, repo_full_name: str, pr_number: int):
|
def get_pr_details(self, repo_full_name: str, pr_number: int):
|
||||||
owner, repo = repo_full_name.split("/")
|
owner, repo = repo_full_name.split("/")
|
||||||
url = f"repos/{owner}/{repo}/pulls/{pr_number}"
|
url = f"repos/{owner}/{repo}/pulls/{pr_number}"
|
||||||
response = self._request("GET", url)
|
response, duration = self._request("GET", url)
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
def create_review(self, repo_full_name: str, pr_number: int, event: str = "APPROVED", body: str = "LGTM"):
|
def create_review(self, repo_full_name: str, pr_number: int, event: str = "APPROVED", body: str = "LGTM"):
|
||||||
@@ -454,7 +483,7 @@ index 0000000..{pkg_b_sha}
|
|||||||
existing_reviews = self.list_reviews(repo_full_name, pr_number)
|
existing_reviews = self.list_reviews(repo_full_name, pr_number)
|
||||||
for r in existing_reviews:
|
for r in existing_reviews:
|
||||||
if r["user"]["login"] == current_user and r["state"] == "APPROVED" and event == "APPROVED":
|
if r["user"]["login"] == current_user and r["state"] == "APPROVED" and event == "APPROVED":
|
||||||
print(f"User {current_user} already has an APPROVED review for {repo_full_name} PR #{pr_number}")
|
vprint(f"User {current_user} already has an APPROVED review for {repo_full_name} PR #{pr_number}")
|
||||||
return r
|
return r
|
||||||
|
|
||||||
url = f"repos/{owner}/{repo}/pulls/{pr_number}/reviews"
|
url = f"repos/{owner}/{repo}/pulls/{pr_number}/reviews"
|
||||||
@@ -462,13 +491,13 @@ index 0000000..{pkg_b_sha}
|
|||||||
"event": event,
|
"event": event,
|
||||||
"body": body
|
"body": body
|
||||||
}
|
}
|
||||||
print(f"--- Creating and submitting review ({event}) for {repo_full_name} PR #{pr_number} as {current_user} ---")
|
vprint(f"--- Creating and submitting review ({event}) for {repo_full_name} PR #{pr_number} as {current_user} ---")
|
||||||
try:
|
try:
|
||||||
response = self._request("POST", url, json=data)
|
response, duration = self._request("POST", url, json=data)
|
||||||
review = response.json()
|
review = response.json()
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
# If it fails with 422, it might be because a review is already pending or something else
|
# If it fails with 422, it might be because a review is already pending or something else
|
||||||
print(f"Failed to create review: {e.response.text}")
|
vprint(f"Failed to create review: {e.response.text}")
|
||||||
# Try to find a pending review to submit
|
# Try to find a pending review to submit
|
||||||
existing_reviews = self.list_reviews(repo_full_name, pr_number)
|
existing_reviews = self.list_reviews(repo_full_name, pr_number)
|
||||||
pending_review = next((r for r in existing_reviews if r["user"]["login"] == current_user and r["state"] == "PENDING"), None)
|
pending_review = next((r for r in existing_reviews if r["user"]["login"] == current_user and r["state"] == "PENDING"), None)
|
||||||
@@ -486,11 +515,11 @@ index 0000000..{pkg_b_sha}
|
|||||||
"body": body
|
"body": body
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
self._request("POST", submit_url, json=submit_data)
|
response, duration = self._request("POST", submit_url, json=submit_data)
|
||||||
print(f"--- Review {review_id} submitted ---")
|
vprint(f"[{duration:.3f}s] --- Review {review_id} submitted ---")
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
if "already" in e.response.text.lower() or "stay pending" in e.response.text.lower():
|
if "already" in e.response.text.lower() or "stay pending" in e.response.text.lower():
|
||||||
print(f"Review {review_id} could not be submitted further: {e.response.text}")
|
vprint(f"Review {review_id} could not be submitted further: {e.response.text}")
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@@ -499,34 +528,65 @@ index 0000000..{pkg_b_sha}
|
|||||||
def list_reviews(self, repo_full_name: str, pr_number: int):
|
def list_reviews(self, repo_full_name: str, pr_number: int):
|
||||||
owner, repo = repo_full_name.split("/")
|
owner, repo = repo_full_name.split("/")
|
||||||
url = f"repos/{owner}/{repo}/pulls/{pr_number}/reviews"
|
url = f"repos/{owner}/{repo}/pulls/{pr_number}/reviews"
|
||||||
response = self._request("GET", url)
|
response, duration = self._request("GET", url)
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
def approve_requested_reviews(self, repo_full_name: str, pr_number: int):
|
def approve_requested_reviews(self, repo_full_name: str, pr_number: int):
|
||||||
print(f"--- Checking for REQUEST_REVIEW state in {repo_full_name} PR #{pr_number} ---")
|
vprint(f"--- Checking for REQUEST_REVIEW state in {repo_full_name} PR #{pr_number} ---")
|
||||||
reviews = self.list_reviews(repo_full_name, pr_number)
|
reviews = self.list_reviews(repo_full_name, pr_number)
|
||||||
|
|
||||||
requested_reviews = [r for r in reviews if r["state"] == "REQUEST_REVIEW"]
|
requested_reviews = [r for r in reviews if r["state"] == "REQUEST_REVIEW"]
|
||||||
if not requested_reviews:
|
if not requested_reviews:
|
||||||
print(f"No reviews in REQUEST_REVIEW state found for {repo_full_name} PR #{pr_number}")
|
vprint(f"No reviews in REQUEST_REVIEW state found for {repo_full_name} PR #{pr_number}")
|
||||||
return
|
return
|
||||||
|
|
||||||
admin_token = self.headers["Authorization"].split(" ")[1]
|
admin_token = self.headers["Authorization"].split(" ")[1]
|
||||||
for r in requested_reviews:
|
for r in requested_reviews:
|
||||||
reviewer_username = r["user"]["login"]
|
reviewer_username = r["user"]["login"]
|
||||||
print(f"Reacting on REQUEST_REVIEW for user {reviewer_username} by approving...")
|
vprint(f"Reacting on REQUEST_REVIEW for user {reviewer_username} by approving...")
|
||||||
|
|
||||||
reviewer_client = GiteaAPIClient(base_url=self.base_url, token=admin_token, sudo=reviewer_username)
|
reviewer_client = GiteaAPIClient(base_url=self.base_url, token=admin_token, sudo=reviewer_username)
|
||||||
time.sleep(1) # give a chance to avoid possible concurrency issues with reviews request/approval
|
time.sleep(1) # give a chance to avoid possible concurrency issues with reviews request/approval
|
||||||
reviewer_client.create_review(repo_full_name, pr_number, event="APPROVED", body="Approving requested review")
|
reviewer_client.create_review(repo_full_name, pr_number, event="APPROVED", body="Approving requested review")
|
||||||
|
|
||||||
def restart_service(self, service_name: str):
|
def wait_for_project_pr(self, package_pr_repo, package_pr_number, project_pr_repo="myproducts/mySLFO", timeout=60):
|
||||||
print(f"--- Restarting service: {service_name} ---")
|
vprint(f"Polling {package_pr_repo} PR #{package_pr_number} timeline for forwarded PR event in {project_pr_repo}...")
|
||||||
try:
|
for _ in range(timeout):
|
||||||
# Assumes podman-compose.yml is in the parent directory of tests/lib
|
time.sleep(1)
|
||||||
subprocess.run(["podman-compose", "restart", service_name], check=True, cwd=os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)))
|
timeline_events = self.get_timeline_events(package_pr_repo, package_pr_number)
|
||||||
print(f"Service {service_name} restarted successfully.")
|
for event in timeline_events:
|
||||||
except subprocess.CalledProcessError as e:
|
if event.get("type") == "pull_ref":
|
||||||
print(f"Error restarting service {service_name}: {e}")
|
if not (ref_issue := event.get("ref_issue")):
|
||||||
raise
|
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):
|
||||||
|
vprint(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
|
||||||
|
vprint(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
|
||||||
|
vprint(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
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import subprocess
|
|
||||||
import requests
|
import requests
|
||||||
from pathlib import Path
|
|
||||||
from tests.lib.common_test_utils import (
|
from tests.lib.common_test_utils import (
|
||||||
GiteaAPIClient,
|
GiteaAPIClient,
|
||||||
mock_build_result,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -17,33 +14,14 @@ from tests.lib.common_test_utils import (
|
|||||||
def test_pr_workflow_succeeded(staging_main_env, mock_build_result):
|
def test_pr_workflow_succeeded(staging_main_env, mock_build_result):
|
||||||
"""End-to-end test for a successful PR workflow."""
|
"""End-to-end test for a successful PR workflow."""
|
||||||
gitea_env, test_full_repo_name, merge_branch_name = staging_main_env
|
gitea_env, test_full_repo_name, merge_branch_name = staging_main_env
|
||||||
diff = "diff --git a/test.txt b/test.txt\nnew file mode 100644\nindex 0000000..e69de29\n"
|
diff = "diff --git a/test.txt b/test.txt\nnew file mode 100644\nindex 00000000..473a0f4c\n"
|
||||||
pr = gitea_env.create_gitea_pr("mypool/pkgA", diff, "Test PR - should succeed", False, base_branch=merge_branch_name)
|
pr = gitea_env.create_gitea_pr("mypool/pkgA", diff, "Test PR - should succeed", False, base_branch=merge_branch_name)
|
||||||
initial_pr_number = pr["number"]
|
initial_pr_number = pr["number"]
|
||||||
|
|
||||||
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 (
|
assert (
|
||||||
forwarded_pr_number is not None
|
forwarded_pr_number is not None
|
||||||
), "Workflow bot did not create a pull_ref event on the timeline."
|
), "Workflow bot did not create a project PR."
|
||||||
print(f"Found forwarded PR: myproducts/mySLFO #{forwarded_pr_number}")
|
print(f"Found forwarded PR: myproducts/mySLFO #{forwarded_pr_number}")
|
||||||
|
|
||||||
print(f"Polling myproducts/mySLFO PR #{forwarded_pr_number} for reviewer assignment...")
|
print(f"Polling myproducts/mySLFO PR #{forwarded_pr_number} for reviewer assignment...")
|
||||||
@@ -60,17 +38,10 @@ def test_pr_workflow_succeeded(staging_main_env, mock_build_result):
|
|||||||
assert reviewer_added, "Staging bot was not added as a reviewer."
|
assert reviewer_added, "Staging bot was not added as a reviewer."
|
||||||
print("Staging bot has been added as a reviewer.")
|
print("Staging bot has been added as a reviewer.")
|
||||||
|
|
||||||
mock_build_result(package_name="pkgA", code="succeeded")
|
mock_build_result(package_name="pkgA", code="succeeded")
|
||||||
|
|
||||||
print("Restarting obs-staging-bot...")
|
|
||||||
subprocess.run(
|
|
||||||
["podman-compose", "restart", "obs-staging-bot"],
|
|
||||||
cwd=compose_dir,
|
|
||||||
check=True,
|
|
||||||
capture_output=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"Polling myproducts/mySLFO PR #{forwarded_pr_number} for final status...")
|
print(f"Polling myproducts/mySLFO PR #{forwarded_pr_number} for final status...")
|
||||||
|
|
||||||
status_comment_found = False
|
status_comment_found = False
|
||||||
for _ in range(20):
|
for _ in range(20):
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
@@ -88,33 +59,14 @@ def test_pr_workflow_succeeded(staging_main_env, mock_build_result):
|
|||||||
def test_pr_workflow_failed(staging_main_env, mock_build_result):
|
def test_pr_workflow_failed(staging_main_env, mock_build_result):
|
||||||
"""End-to-end test for a failed PR workflow."""
|
"""End-to-end test for a failed PR workflow."""
|
||||||
gitea_env, test_full_repo_name, merge_branch_name = staging_main_env
|
gitea_env, test_full_repo_name, merge_branch_name = staging_main_env
|
||||||
diff = "diff --git a/another_test.txt b/another_test.txt\nnew file mode 100644\nindex 0000000..e69de29\n"
|
diff = "diff --git a/another_test.txt b/another_test.txt\nnew file mode 100644\nindex 00000000..473a0f4c\n"
|
||||||
pr = gitea_env.create_gitea_pr("mypool/pkgA", diff, "Test PR - should fail", False, base_branch=merge_branch_name)
|
pr = gitea_env.create_gitea_pr("mypool/pkgA", diff, "Test PR - should fail", False, base_branch=merge_branch_name)
|
||||||
initial_pr_number = pr["number"]
|
initial_pr_number = pr["number"]
|
||||||
|
|
||||||
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 (
|
assert (
|
||||||
forwarded_pr_number is not None
|
forwarded_pr_number is not None
|
||||||
), "Workflow bot did not create a pull_ref event on the timeline."
|
), "Workflow bot did not create a project PR."
|
||||||
print(f"Found forwarded PR: myproducts/mySLFO #{forwarded_pr_number}")
|
print(f"Found forwarded PR: myproducts/mySLFO #{forwarded_pr_number}")
|
||||||
|
|
||||||
print(f"Polling myproducts/mySLFO PR #{forwarded_pr_number} for reviewer assignment...")
|
print(f"Polling myproducts/mySLFO PR #{forwarded_pr_number} for reviewer assignment...")
|
||||||
@@ -133,14 +85,6 @@ def test_pr_workflow_failed(staging_main_env, mock_build_result):
|
|||||||
|
|
||||||
mock_build_result(package_name="pkgA", code="failed")
|
mock_build_result(package_name="pkgA", code="failed")
|
||||||
|
|
||||||
print("Restarting obs-staging-bot...")
|
|
||||||
subprocess.run(
|
|
||||||
["podman-compose", "restart", "obs-staging-bot"],
|
|
||||||
cwd=compose_dir,
|
|
||||||
check=True,
|
|
||||||
capture_output=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"Polling myproducts/mySLFO PR #{forwarded_pr_number} for final status...")
|
print(f"Polling myproducts/mySLFO PR #{forwarded_pr_number} for final status...")
|
||||||
status_comment_found = False
|
status_comment_found = False
|
||||||
for _ in range(20):
|
for _ in range(20):
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ def test_001_project_pr_labels(label_env, staging_bot_client):
|
|||||||
# 1. Create a package PR
|
# 1. Create a package PR
|
||||||
diff = """diff --git a/label_test_fixture.txt b/label_test_fixture.txt
|
diff = """diff --git a/label_test_fixture.txt b/label_test_fixture.txt
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000..e69de29
|
index 00000000..473a0f4c
|
||||||
"""
|
"""
|
||||||
print(f"--- Creating package PR in mypool/pkgA on branch {branch_name} ---")
|
print(f"--- Creating package PR in mypool/pkgA on branch {branch_name} ---")
|
||||||
package_pr = gitea_env.create_gitea_pr("mypool/pkgA", diff, "Test Labels Fixture", False, base_branch=branch_name)
|
package_pr = gitea_env.create_gitea_pr("mypool/pkgA", diff, "Test Labels Fixture", False, base_branch=branch_name)
|
||||||
@@ -35,23 +35,7 @@ index 0000000..e69de29
|
|||||||
print(f"Created package PR mypool/pkgA#{package_pr_number}")
|
print(f"Created package PR mypool/pkgA#{package_pr_number}")
|
||||||
|
|
||||||
# 2. Make sure the workflow-pr service created related project PR
|
# 2. Make sure the workflow-pr service created related project PR
|
||||||
project_pr_number = None
|
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", package_pr_number)
|
||||||
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."
|
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}")
|
print(f"Found project PR: myproducts/mySLFO#{project_pr_number}")
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def test_001_automerge(automerge_env, test_user_client):
|
|||||||
# 1. Create a package PR
|
# 1. Create a package PR
|
||||||
diff = """diff --git a/automerge_test.txt b/automerge_test.txt
|
diff = """diff --git a/automerge_test.txt b/automerge_test.txt
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000..e69de29
|
index 00000000..473a0f4c
|
||||||
"""
|
"""
|
||||||
print(f"--- Creating package PR in mypool/pkgA on branch {merge_branch_name} ---")
|
print(f"--- Creating package PR in mypool/pkgA on branch {merge_branch_name} ---")
|
||||||
package_pr = test_user_client.create_gitea_pr("mypool/pkgA", diff, "Test Automerge Fixture", False, base_branch=merge_branch_name)
|
package_pr = test_user_client.create_gitea_pr("mypool/pkgA", diff, "Test Automerge Fixture", False, base_branch=merge_branch_name)
|
||||||
@@ -25,59 +25,18 @@ index 0000000..e69de29
|
|||||||
print(f"Created package PR mypool/pkgA#{package_pr_number}")
|
print(f"Created package PR mypool/pkgA#{package_pr_number}")
|
||||||
|
|
||||||
# 2. Make sure the workflow-pr service created related project PR
|
# 2. Make sure the workflow-pr service created related project PR
|
||||||
project_pr_number = None
|
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", package_pr_number)
|
||||||
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."
|
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}")
|
print(f"Found project PR: myproducts/mySLFO#{project_pr_number}")
|
||||||
|
|
||||||
# 3. Approve reviews and verify merged
|
# 3. Approve reviews and verify merged
|
||||||
print("Approving reviews and verifying both PRs are merged...")
|
pkg_merged, prj_merged = gitea_env.approve_and_wait_merge("mypool/pkgA", package_pr_number, project_pr_number)
|
||||||
package_merged = False
|
assert pkg_merged, f"Package PR mypool/pkgA#{package_pr_number} was not merged automatically."
|
||||||
project_merged = False
|
assert prj_merged, f"Project PR myproducts/mySLFO#{project_pr_number} was not merged automatically."
|
||||||
|
|
||||||
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.")
|
print("Both PRs merged successfully.")
|
||||||
|
|
||||||
@pytest.mark.t002
|
@pytest.mark.t002
|
||||||
def test_002_manual_merge(manual_merge_env, test_user_client, usera_client, staging_bot_client):
|
def test_002_manual_merge(manual_merge_env, test_user_client, usera_client, staging_bot_client, ownerA_client):
|
||||||
"""
|
"""
|
||||||
Test scenario TC-MERGE-002:
|
Test scenario TC-MERGE-002:
|
||||||
1. Create a PackageGit PR with ManualMergeOnly set to true.
|
1. Create a PackageGit PR with ManualMergeOnly set to true.
|
||||||
@@ -90,42 +49,27 @@ def test_002_manual_merge(manual_merge_env, test_user_client, usera_client, stag
|
|||||||
# 1. Create a package PR
|
# 1. Create a package PR
|
||||||
diff = """diff --git a/manual_merge_test.txt b/manual_merge_test.txt
|
diff = """diff --git a/manual_merge_test.txt b/manual_merge_test.txt
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000..e69de29
|
index 00000000..473a0f4c
|
||||||
"""
|
"""
|
||||||
print(f"--- Creating package PR in mypool/pkgA on branch {merge_branch_name} ---")
|
print(f"--- Creating package PR in mypool/pkgA on branch {merge_branch_name} ---")
|
||||||
package_pr = test_user_client.create_gitea_pr("mypool/pkgA", diff, "Test Manual Merge Fixture", False, base_branch=merge_branch_name)
|
package_pr = gitea_env.create_gitea_pr("mypool/pkgA", diff, "Test Manual Merge Fixture", False, base_branch=merge_branch_name)
|
||||||
package_pr_number = package_pr["number"]
|
package_pr_number = package_pr["number"]
|
||||||
print(f"Created package PR mypool/pkgA#{package_pr_number}")
|
print(f"Created package PR mypool/pkgA#{package_pr_number}")
|
||||||
|
|
||||||
# 2. Make sure the workflow-pr service created related project PR
|
# 2. Make sure the workflow-pr service created related project PR
|
||||||
project_pr_number = None
|
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", package_pr_number)
|
||||||
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."
|
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}")
|
print(f"Found project PR: myproducts/mySLFO#{project_pr_number}")
|
||||||
|
|
||||||
# 3. Approve reviews and verify NOT merged
|
# 3. Approve reviews and verify NOT merged
|
||||||
print("Waiting for all expected review requests and approving them...")
|
print("Waiting for required review requests and approving them...")
|
||||||
# Expected reviewers based on manual-merge branch config and pkgA maintainership
|
# Expected reviewers based on manual-merge branch config and pkgA maintainership
|
||||||
expected_reviewers = {"usera", "userb", "ownerA", "ownerX", "ownerY"}
|
mandatory_reviewers = {"usera", "userb"}
|
||||||
|
maintainers = {"ownerA", "ownerX", "ownerY"}
|
||||||
|
|
||||||
# ManualMergeOnly still requires regular reviews to be satisfied.
|
# ManualMergeOnly still requires regular reviews to be satisfied.
|
||||||
# We poll until all expected reviewers are requested, then approve them.
|
# We poll until required reviewers have approved.
|
||||||
all_requested = False
|
all_approved = False
|
||||||
for _ in range(30):
|
for _ in range(30):
|
||||||
# Trigger approvals for whatever is already requested
|
# Trigger approvals for whatever is already requested
|
||||||
gitea_env.approve_requested_reviews("mypool/pkgA", package_pr_number)
|
gitea_env.approve_requested_reviews("mypool/pkgA", package_pr_number)
|
||||||
@@ -137,20 +81,17 @@ index 0000000..e69de29
|
|||||||
print("Staging bot has a pending/requested review. Approving...")
|
print("Staging bot has a pending/requested review. Approving...")
|
||||||
staging_bot_client.create_review("myproducts/mySLFO", project_pr_number, event="APPROVED", body="Staging bot approves")
|
staging_bot_client.create_review("myproducts/mySLFO", project_pr_number, event="APPROVED", body="Staging bot approves")
|
||||||
|
|
||||||
# Check if all expected reviewers have at least one review record (any state)
|
# Check if mandatory reviewers and at least one maintainer have approved
|
||||||
pkg_reviews = gitea_env.list_reviews("mypool/pkgA", package_pr_number)
|
pkg_reviews = gitea_env.list_reviews("mypool/pkgA", package_pr_number)
|
||||||
current_reviewers = {r["user"]["login"] for r in pkg_reviews}
|
approved_reviewers = {r["user"]["login"] for r in pkg_reviews if r["state"] == "APPROVED"}
|
||||||
|
|
||||||
if expected_reviewers.issubset(current_reviewers):
|
if mandatory_reviewers.issubset(approved_reviewers) and any(m in approved_reviewers for m in maintainers):
|
||||||
# Also ensure they are all approved (not just requested)
|
# And check project PR for bot approval
|
||||||
approved_reviewers = {r["user"]["login"] for r in pkg_reviews if r["state"] == "APPROVED"}
|
prj_approved = any(r["user"]["login"] == "autogits_obs_staging_bot" and r["state"] == "APPROVED" for r in prj_reviews)
|
||||||
if expected_reviewers.issubset(approved_reviewers):
|
if prj_approved:
|
||||||
# And check project PR for bot approval
|
all_approved = True
|
||||||
prj_approved = any(r["user"]["login"] == "autogits_obs_staging_bot" and r["state"] == "APPROVED" for r in prj_reviews)
|
print(f"Required reviewers approved: mandatory={mandatory_reviewers}, maintainer={[m for m in maintainers if m in approved_reviewers]}, staging_bot=True")
|
||||||
if prj_approved:
|
break
|
||||||
all_requested = True
|
|
||||||
print(f"All expected reviewers {expected_reviewers} and staging bot have approved.")
|
|
||||||
break
|
|
||||||
|
|
||||||
pkg_details = gitea_env.get_pr_details("mypool/pkgA", package_pr_number)
|
pkg_details = gitea_env.get_pr_details("mypool/pkgA", package_pr_number)
|
||||||
prj_details = gitea_env.get_pr_details("myproducts/mySLFO", project_pr_number)
|
prj_details = gitea_env.get_pr_details("myproducts/mySLFO", project_pr_number)
|
||||||
@@ -160,12 +101,12 @@ index 0000000..e69de29
|
|||||||
|
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
assert all_requested, f"Timed out waiting for all expected reviewers {expected_reviewers} to approve. Current: {current_reviewers}"
|
assert all_approved, f"Timed out waiting for required approvals. Mandatory: {mandatory_reviewers}, Maintainers: {maintainers}. Current approved: {approved_reviewers}"
|
||||||
print("Both PRs have all required approvals but are not merged (as expected with ManualMergeOnly).")
|
print("Both PRs have all required approvals but are not merged (as expected with ManualMergeOnly).")
|
||||||
|
|
||||||
# 4. Comment "merge ok" from a requested reviewer (usera)
|
# 4. Comment "merge ok" from a requested reviewer (ownerA)
|
||||||
print("Commenting 'merge ok' on package PR...")
|
print("Commenting 'merge ok' on package PR from a maintainer...")
|
||||||
usera_client.create_issue_comment("mypool/pkgA", package_pr_number, "merge ok")
|
ownerA_client.create_issue_comment("mypool/pkgA", package_pr_number, "merge ok")
|
||||||
|
|
||||||
# 5. Verify both PRs are merged
|
# 5. Verify both PRs are merged
|
||||||
print("Polling for PR merge status...")
|
print("Polling for PR merge status...")
|
||||||
@@ -193,3 +134,359 @@ index 0000000..e69de29
|
|||||||
assert package_merged, f"Package PR mypool/pkgA#{package_pr_number} was not merged after 'merge ok'."
|
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'."
|
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'.")
|
print("Both PRs merged successfully after 'merge ok'.")
|
||||||
|
|
||||||
|
@pytest.mark.t003
|
||||||
|
def test_003_refuse_manual_merge(manual_merge_env, test_user_client, ownerB_client, staging_bot_client):
|
||||||
|
"""
|
||||||
|
Test scenario TC-MERGE-003:
|
||||||
|
1. Create a PackageGit PR with ManualMergeOnly set to true.
|
||||||
|
2. Ensure all mandatory reviews are completed on both project and package PRs.
|
||||||
|
3. Comment "merge ok" on the package PR from the account of a not requested reviewer.
|
||||||
|
4. Verify the PR is not merged.
|
||||||
|
"""
|
||||||
|
gitea_env, test_full_repo_name, merge_branch_name = manual_merge_env
|
||||||
|
|
||||||
|
# 1. Create a package PR
|
||||||
|
diff = """diff --git a/manual_merge_test.txt b/manual_merge_test.txt
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..473a0f4c
|
||||||
|
"""
|
||||||
|
print(f"--- Creating package PR in mypool/pkgA on branch {merge_branch_name} ---")
|
||||||
|
package_pr = test_user_client.create_gitea_pr("mypool/pkgA", diff, "Test Manual Merge Fixture", False, base_branch=merge_branch_name)
|
||||||
|
package_pr_number = package_pr["number"]
|
||||||
|
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)
|
||||||
|
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 NOT merged
|
||||||
|
print("Waiting for required review requests and approving them...")
|
||||||
|
# Expected reviewers based on manual-merge branch config and pkgA maintainership
|
||||||
|
mandatory_reviewers = {"usera", "userb"}
|
||||||
|
maintainers = {"ownerA", "ownerX", "ownerY"}
|
||||||
|
|
||||||
|
# ManualMergeOnly still requires regular reviews to be satisfied.
|
||||||
|
# We poll until required reviewers have approved.
|
||||||
|
all_approved = False
|
||||||
|
for _ in range(30):
|
||||||
|
# Trigger approvals for whatever is already requested
|
||||||
|
gitea_env.approve_requested_reviews("mypool/pkgA", package_pr_number)
|
||||||
|
gitea_env.approve_requested_reviews("myproducts/mySLFO", project_pr_number)
|
||||||
|
|
||||||
|
# Explicitly handle staging bot if it is requested or pending
|
||||||
|
prj_reviews = gitea_env.list_reviews("myproducts/mySLFO", project_pr_number)
|
||||||
|
if any(r["user"]["login"] == "autogits_obs_staging_bot" and r["state"] in ["REQUEST_REVIEW", "PENDING"] for r in prj_reviews):
|
||||||
|
print("Staging bot has a pending/requested review. Approving...")
|
||||||
|
staging_bot_client.create_review("myproducts/mySLFO", project_pr_number, event="APPROVED", body="Staging bot approves")
|
||||||
|
|
||||||
|
# Check if mandatory reviewers and at least one maintainer have approved
|
||||||
|
pkg_reviews = gitea_env.list_reviews("mypool/pkgA", package_pr_number)
|
||||||
|
approved_reviewers = {r["user"]["login"] for r in pkg_reviews if r["state"] == "APPROVED"}
|
||||||
|
|
||||||
|
if mandatory_reviewers.issubset(approved_reviewers) and any(m in approved_reviewers for m in maintainers):
|
||||||
|
# And check project PR for bot approval
|
||||||
|
prj_approved = any(r["user"]["login"] == "autogits_obs_staging_bot" and r["state"] == "APPROVED" for r in prj_reviews)
|
||||||
|
if prj_approved:
|
||||||
|
all_approved = True
|
||||||
|
print(f"Required reviewers approved: mandatory={mandatory_reviewers}, maintainer={[m for m in maintainers if m in approved_reviewers]}, staging_bot=True")
|
||||||
|
break
|
||||||
|
|
||||||
|
pkg_details = gitea_env.get_pr_details("mypool/pkgA", package_pr_number)
|
||||||
|
prj_details = gitea_env.get_pr_details("myproducts/mySLFO", project_pr_number)
|
||||||
|
|
||||||
|
assert not pkg_details.get("merged"), "Package PR merged prematurely (ManualMergeOnly ignored?)"
|
||||||
|
assert not prj_details.get("merged"), "Project PR merged prematurely (ManualMergeOnly ignored?)"
|
||||||
|
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
assert all_approved, f"Timed out waiting for required approvals. Mandatory: {mandatory_reviewers}, Maintainers: {maintainers}. Current approved: {approved_reviewers}"
|
||||||
|
print("Both PRs have all required approvals but are not merged (as expected with ManualMergeOnly).")
|
||||||
|
|
||||||
|
# 4. Comment "merge ok" from a requested reviewer (ownerB)
|
||||||
|
print("Commenting 'merge ok' on package PR as user ownerB ...")
|
||||||
|
ownerB_client.create_issue_comment("mypool/pkgA", package_pr_number, "merge ok")
|
||||||
|
|
||||||
|
# 5. Verify both PRs are merged
|
||||||
|
print("Polling for PR merge status...")
|
||||||
|
package_merged = False
|
||||||
|
project_merged = False
|
||||||
|
|
||||||
|
for i in range(20): # Poll for up to 20 seconds
|
||||||
|
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.")
|
||||||
|
|
||||||
|
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 not package_merged, f"Package PR mypool/pkgA#{package_pr_number} was merged after 'merge ok'."
|
||||||
|
assert not project_merged, f"Project PR myproducts/mySLFO#{project_pr_number} was merged after 'merge ok'."
|
||||||
|
print("Both PRs merged not 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 00000000..473a0f4c
|
||||||
|
"""
|
||||||
|
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 00000000..473a0f4c
|
||||||
|
--- /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 00000000..473a0f4c\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 00000000..473a0f4c
|
||||||
|
--- /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 00000000..473a0f4c
|
||||||
|
--- /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
|
||||||
|
resp, _ = gitea_env._request("GET", f"repos/myproducts/mySLFO/branches/{merge_branch_name}")
|
||||||
|
branch_info = resp.json()
|
||||||
|
new_head_sha = branch_info["commit"]["id"]
|
||||||
|
|
||||||
|
resp, _ = gitea_env._request("GET", f"repos/myproducts/mySLFO/git/commits/{new_head_sha}")
|
||||||
|
commit_details = resp.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']}"
|
||||||
|
|
||||||
|
@pytest.mark.t012
|
||||||
|
def test_012_merge_mode_devel_ff_success(merge_devel_env, ownerA_client):
|
||||||
|
"""
|
||||||
|
Test MergeMode "devel" - Success case (No Conflict, should fast-forward)
|
||||||
|
"""
|
||||||
|
gitea_env, test_full_repo_name, merge_branch_name = merge_devel_env
|
||||||
|
|
||||||
|
ts = time.strftime("%H%M%S")
|
||||||
|
filename = f"devel_ff_test_{ts}.txt"
|
||||||
|
|
||||||
|
# 1. Create a package PR (this will be FF-mergeable by default)
|
||||||
|
diff = f"""diff --git a/{filename} b/{filename}
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..473a0f4c
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/{filename}
|
||||||
|
@@ -0,0 +1 @@
|
||||||
|
+PR content
|
||||||
|
"""
|
||||||
|
package_pr = ownerA_client.create_gitea_pr("mypool/pkgA", diff, "Test Devel 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_details = gitea_env.get_pr_details("mypool/pkgA", package_pr_number)
|
||||||
|
pkg_head_sha = pkg_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 FF merge successful.")
|
||||||
|
|
||||||
|
# Verify that the package base branch HEAD is the same as the PR head (FF)
|
||||||
|
resp, _ = gitea_env._request("GET", f"repos/mypool/pkgA/branches/{merge_branch_name}")
|
||||||
|
branch_info = resp.json()
|
||||||
|
new_head_sha = branch_info["commit"]["id"]
|
||||||
|
assert new_head_sha == pkg_head_sha, f"Package branch {merge_branch_name} HEAD should be {pkg_head_sha} but is {new_head_sha}"
|
||||||
|
|
||||||
|
resp, _ = gitea_env._request("GET", f"repos/mypool/pkgA/git/commits/{new_head_sha}")
|
||||||
|
commit_details = resp.json()
|
||||||
|
assert len(commit_details["parents"]) == 1, f"Package branch {merge_branch_name} HEAD should have 1 parent but has {len(commit_details['parents'])}"
|
||||||
|
|
||||||
|
@pytest.mark.t013
|
||||||
|
def test_013_merge_mode_replace_ff_success(merge_replace_env, ownerA_client):
|
||||||
|
"""
|
||||||
|
Test MergeMode "replace" - Success case (No Conflict, should fast-forward)
|
||||||
|
"""
|
||||||
|
gitea_env, test_full_repo_name, merge_branch_name = merge_replace_env
|
||||||
|
|
||||||
|
ts = time.strftime("%H%M%S")
|
||||||
|
filename = f"replace_ff_test_{ts}.txt"
|
||||||
|
|
||||||
|
# 1. Create a package PR (this will be FF-mergeable by default)
|
||||||
|
diff = f"""diff --git a/{filename} b/{filename}
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..473a0f4c
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/{filename}
|
||||||
|
@@ -0,0 +1 @@
|
||||||
|
+PR content
|
||||||
|
"""
|
||||||
|
package_pr = ownerA_client.create_gitea_pr("mypool/pkgA", diff, "Test Replace 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_details = gitea_env.get_pr_details("mypool/pkgA", package_pr_number)
|
||||||
|
pkg_head_sha = pkg_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("Replace FF merge successful.")
|
||||||
|
|
||||||
|
# Verify that the package base branch HEAD is the same as the PR head (FF)
|
||||||
|
resp, _ = gitea_env._request("GET", f"repos/mypool/pkgA/branches/{merge_branch_name}")
|
||||||
|
branch_info = resp.json()
|
||||||
|
new_head_sha = branch_info["commit"]["id"]
|
||||||
|
assert new_head_sha == pkg_head_sha, f"Package branch {merge_branch_name} HEAD should be {pkg_head_sha} but is {new_head_sha}"
|
||||||
|
|
||||||
|
resp, _ = gitea_env._request("GET", f"repos/mypool/pkgA/git/commits/{new_head_sha}")
|
||||||
|
commit_details = resp.json()
|
||||||
|
assert len(commit_details["parents"]) == 1, f"Package branch {merge_branch_name} HEAD should have 1 parent but has {len(commit_details['parents'])}"
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ def test_001_review_requests_matching_config(automerge_env, ownerA_client):
|
|||||||
filename = f"pkgB_test_{ts}.txt"
|
filename = f"pkgB_test_{ts}.txt"
|
||||||
diff = f"""diff --git a/{filename} b/{filename}
|
diff = f"""diff --git a/{filename} b/{filename}
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000..e69de29
|
index 00000000..473a0f4c
|
||||||
"""
|
"""
|
||||||
print(f"--- Creating package PR in mypool/pkgB on branch {branch_name} as ownerA ---")
|
print(f"--- Creating package PR in mypool/pkgB on branch {branch_name} as ownerA ---")
|
||||||
package_pr = ownerA_client.create_gitea_pr("mypool/pkgB", diff, "Test Review Requests Config", True, base_branch=branch_name)
|
package_pr = ownerA_client.create_gitea_pr("mypool/pkgB", diff, "Test Review Requests Config", True, base_branch=branch_name)
|
||||||
@@ -86,7 +86,7 @@ def test_004_maintainer(maintainer_env, ownerA_client):
|
|||||||
filename = f"maintainer_test_{ts}.txt"
|
filename = f"maintainer_test_{ts}.txt"
|
||||||
diff = f"""diff --git a/{filename} b/{filename}
|
diff = f"""diff --git a/{filename} b/{filename}
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000..e69de29
|
index 00000000..473a0f4c
|
||||||
"""
|
"""
|
||||||
print(f"--- Creating package PR in mypool/pkgA on branch {branch_name} as ownerA ---")
|
print(f"--- Creating package PR in mypool/pkgA on branch {branch_name} as ownerA ---")
|
||||||
package_pr = ownerA_client.create_gitea_pr("mypool/pkgA", diff, "Test Maintainer Merge", True, base_branch=branch_name)
|
package_pr = ownerA_client.create_gitea_pr("mypool/pkgA", diff, "Test Maintainer Merge", True, base_branch=branch_name)
|
||||||
@@ -94,23 +94,7 @@ index 0000000..e69de29
|
|||||||
print(f"Created package PR mypool/pkgA#{package_pr_number}")
|
print(f"Created package PR mypool/pkgA#{package_pr_number}")
|
||||||
|
|
||||||
# 2. Make sure the workflow-pr service created related project PR
|
# 2. Make sure the workflow-pr service created related project PR
|
||||||
project_pr_number = None
|
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", package_pr_number)
|
||||||
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."
|
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}")
|
print(f"Found project PR: myproducts/mySLFO#{project_pr_number}")
|
||||||
|
|
||||||
@@ -171,7 +155,7 @@ def test_005_any_maintainer_approval_sufficient(maintainer_env, ownerA_client, o
|
|||||||
filename = f"pkgB_test_{ts}.txt"
|
filename = f"pkgB_test_{ts}.txt"
|
||||||
diff = f"""diff --git a/{filename} b/{filename}
|
diff = f"""diff --git a/{filename} b/{filename}
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000..e69de29
|
index 00000000..473a0f4c
|
||||||
"""
|
"""
|
||||||
print(f"--- Creating package PR in mypool/pkgB on branch {branch_name} as ownerA ---")
|
print(f"--- Creating package PR in mypool/pkgB on branch {branch_name} as ownerA ---")
|
||||||
package_pr = ownerA_client.create_gitea_pr("mypool/pkgB", diff, "Test Single Maintainer Merge", True, base_branch=branch_name)
|
package_pr = ownerA_client.create_gitea_pr("mypool/pkgB", diff, "Test Single Maintainer Merge", True, base_branch=branch_name)
|
||||||
@@ -179,23 +163,7 @@ index 0000000..e69de29
|
|||||||
print(f"Created package PR mypool/pkgB#{package_pr_number}")
|
print(f"Created package PR mypool/pkgB#{package_pr_number}")
|
||||||
|
|
||||||
# 2. Make sure the workflow-pr service created related project PR
|
# 2. Make sure the workflow-pr service created related project PR
|
||||||
project_pr_number = None
|
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgB", package_pr_number)
|
||||||
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."
|
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}")
|
print(f"Found project PR: myproducts/mySLFO#{project_pr_number}")
|
||||||
|
|
||||||
@@ -249,7 +217,7 @@ def test_006_maintainer_rejection_removes_other_requests(maintainer_env, ownerA_
|
|||||||
filename = f"pkgB_rejection_test_{ts}.txt"
|
filename = f"pkgB_rejection_test_{ts}.txt"
|
||||||
diff = f"""diff --git a/{filename} b/{filename}
|
diff = f"""diff --git a/{filename} b/{filename}
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000..e69de29
|
index 00000000..473a0f4c
|
||||||
"""
|
"""
|
||||||
print(f"--- Creating package PR in mypool/pkgB on branch {branch_name} as ownerA ---")
|
print(f"--- Creating package PR in mypool/pkgB on branch {branch_name} as ownerA ---")
|
||||||
package_pr = ownerA_client.create_gitea_pr("mypool/pkgB", diff, "Test Maintainer Rejection", True, base_branch=branch_name)
|
package_pr = ownerA_client.create_gitea_pr("mypool/pkgB", diff, "Test Maintainer Rejection", True, base_branch=branch_name)
|
||||||
@@ -309,7 +277,7 @@ def test_007_review_required_needs_all_approvals(review_required_env, ownerA_cli
|
|||||||
filename = f"pkgB_review_required_test_{ts}.txt"
|
filename = f"pkgB_review_required_test_{ts}.txt"
|
||||||
diff = f"""diff --git a/{filename} b/{filename}
|
diff = f"""diff --git a/{filename} b/{filename}
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000..e69de29
|
index 00000000..473a0f4c
|
||||||
"""
|
"""
|
||||||
print(f"--- Creating package PR in mypool/pkgB on branch {branch_name} as ownerA ---")
|
print(f"--- Creating package PR in mypool/pkgB on branch {branch_name} as ownerA ---")
|
||||||
package_pr = ownerA_client.create_gitea_pr("mypool/pkgB", diff, "Test Review Required", True, base_branch=branch_name)
|
package_pr = ownerA_client.create_gitea_pr("mypool/pkgB", diff, "Test Review Required", True, base_branch=branch_name)
|
||||||
@@ -317,23 +285,7 @@ index 0000000..e69de29
|
|||||||
print(f"Created package PR mypool/pkgB#{package_pr_number}")
|
print(f"Created package PR mypool/pkgB#{package_pr_number}")
|
||||||
|
|
||||||
# 2. Make sure the workflow-pr service created related project PR
|
# 2. Make sure the workflow-pr service created related project PR
|
||||||
project_pr_number = None
|
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgB", package_pr_number)
|
||||||
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."
|
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}")
|
print(f"Found project PR: myproducts/mySLFO#{project_pr_number}")
|
||||||
|
|
||||||
|
|||||||
@@ -22,32 +22,13 @@ pytest.forwarded_pr_number = None
|
|||||||
@pytest.mark.dependency()
|
@pytest.mark.dependency()
|
||||||
def test_001_project_pr(gitea_env):
|
def test_001_project_pr(gitea_env):
|
||||||
"""Forwarded PR correct title"""
|
"""Forwarded PR correct title"""
|
||||||
diff = "diff --git a/another_test.txt b/another_test.txt\nnew file mode 100644\nindex 0000000..e69de29\n"
|
diff = "diff --git a/another_test.txt b/another_test.txt\nnew file mode 100644\nindex 00000000..473a0f4c\n"
|
||||||
pytest.pr = gitea_env.create_gitea_pr("mypool/pkgA", diff, "Test PR", False)
|
pytest.pr = gitea_env.create_gitea_pr("mypool/pkgA", diff, "Test PR", False)
|
||||||
pytest.initial_pr_number = pytest.pr["number"]
|
pytest.initial_pr_number = pytest.pr["number"]
|
||||||
time.sleep(5) # Give Gitea some time to process the PR and make the timeline available
|
time.sleep(5) # Give Gitea some time to process the PR and make the timeline available
|
||||||
|
|
||||||
compose_dir = Path(__file__).parent.parent
|
pytest.forwarded_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", pytest.initial_pr_number)
|
||||||
|
|
||||||
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 (
|
assert (
|
||||||
pytest.forwarded_pr_number is not None
|
pytest.forwarded_pr_number is not None
|
||||||
), "Workflow bot did not create a forwarded PR."
|
), "Workflow bot did not create a forwarded PR."
|
||||||
@@ -133,7 +114,7 @@ def test_005_NoProjectGitPR_edits_disabled(no_project_git_pr_env, test_user_clie
|
|||||||
# 1. Create a Package PR (without "Allow edits from maintainers" enabled)
|
# 1. Create a Package PR (without "Allow edits from maintainers" enabled)
|
||||||
initial_diff = """diff --git a/first_file.txt b/first_file.txt
|
initial_diff = """diff --git a/first_file.txt b/first_file.txt
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000..e69de29
|
index 00000000..473a0f4c
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
+++ b/first_file.txt
|
+++ b/first_file.txt
|
||||||
@@ -0,0 +1 @@
|
@@ -0,0 +1 @@
|
||||||
@@ -144,23 +125,9 @@ index 0000000..e69de29
|
|||||||
print(f"Created Package PR #{package_pr_number}")
|
print(f"Created Package PR #{package_pr_number}")
|
||||||
|
|
||||||
# 2. Verify that the workflow-pr bot did not create a Project PR
|
# 2. Verify that the workflow-pr bot did not create a Project PR
|
||||||
project_pr_created = False
|
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", package_pr_number, timeout=10)
|
||||||
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 not project_pr_created, "Workflow bot unexpectedly created a Project PR in myproducts/mySLFO."
|
assert project_pr_number is None, "Workflow bot unexpectedly created a Project PR in myproducts/mySLFO."
|
||||||
print("Verification complete: No Project PR was created by the bot.")
|
print("Verification complete: No Project PR was created by the bot.")
|
||||||
|
|
||||||
# 3. Manually create the Project PR
|
# 3. Manually create the Project PR
|
||||||
@@ -193,7 +160,7 @@ index {pkgA_main_sha[:7]}..{pkgA_pr_head_sha[:7]} 160000
|
|||||||
# 4. Trigger an update on the Package PR to prompt the bot to react to the manual Project PR
|
# 4. Trigger an update on the Package PR to prompt the bot to react to the manual Project PR
|
||||||
new_diff_content = """diff --git a/trigger_bot.txt b/trigger_bot.txt
|
new_diff_content = """diff --git a/trigger_bot.txt b/trigger_bot.txt
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000..e69de29
|
index 00000000..473a0f4c
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
+++ b/trigger_bot.txt
|
+++ b/trigger_bot.txt
|
||||||
@@ -0,0 +1 @@
|
@@ -0,0 +1 @@
|
||||||
@@ -233,7 +200,7 @@ def test_006_NoProjectGitPR_edits_enabled(no_project_git_pr_env, test_user_clien
|
|||||||
# 2. Create a Package PR with "Allow edits from maintainers" enabled
|
# 2. Create a Package PR with "Allow edits from maintainers" enabled
|
||||||
diff = """diff --git a/new_feature.txt b/new_feature.txt
|
diff = """diff --git a/new_feature.txt b/new_feature.txt
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000..e69de29
|
index 00000000..473a0f4c
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
+++ b/new_feature.txt
|
+++ b/new_feature.txt
|
||||||
@@ -0,0 +1 @@
|
@@ -0,0 +1 @@
|
||||||
@@ -252,24 +219,9 @@ index 0000000..e69de29
|
|||||||
pkgA_pr_head_sha = package_pr_details["head"]["sha"]
|
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
|
# 3. Assert that the workflow-pr bot did not create a Project PR in the myproducts/mySLFO repository
|
||||||
project_pr_created = False
|
project_pr_number = gitea_env.wait_for_project_pr("mypool/pkgA", package_pr_number, timeout=10)
|
||||||
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 not project_pr_created, "Workflow bot unexpectedly created a Project PR in myproducts/mySLFO."
|
assert project_pr_number is None, "Workflow bot unexpectedly created a Project PR in myproducts/mySLFO."
|
||||||
print("Verification complete: No Project PR was created in myproducts/mySLFO as expected.")
|
print("Verification complete: No Project PR was created in myproducts/mySLFO as expected.")
|
||||||
|
|
||||||
# 1. Create that Project PR from the test code.
|
# 1. Create that Project PR from the test code.
|
||||||
@@ -322,5 +274,3 @@ index 0000000..f587a12
|
|||||||
|
|
||||||
assert project_pr_updated, "Manually created Project PR was not updated by the bot."
|
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.")
|
print("Verification complete: Manually created Project PR was updated by the bot as expected.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,16 +19,19 @@ export GITEA_TOKEN
|
|||||||
echo "GITEA_TOKEN exported (length: ${#GITEA_TOKEN})"
|
echo "GITEA_TOKEN exported (length: ${#GITEA_TOKEN})"
|
||||||
|
|
||||||
# Wait for the dummy data to be created by the gitea setup script
|
# Wait for the dummy data to be created by the gitea setup script
|
||||||
echo "Waiting for workflow.config in myproducts/mySLFO..."
|
echo "Waiting for workflow.config in myproducts/mySLFO (branch zz-ready-to-start)..."
|
||||||
API_URL="http://gitea-test:3000/api/v1/repos/myproducts/mySLFO/contents/workflow.config"
|
API_URL="http://gitea-test:3000/api/v1/repos/myproducts/mySLFO/contents/workflow.config?ref=zz-ready-to-start"
|
||||||
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $GITEA_TOKEN" "$API_URL")
|
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $GITEA_TOKEN" "$API_URL")
|
||||||
|
|
||||||
|
WAITED=false
|
||||||
while [ "$HTTP_STATUS" != "200" ]; do
|
while [ "$HTTP_STATUS" != "200" ]; do
|
||||||
echo "workflow.config not found yet (HTTP Status: $HTTP_STATUS). Retrying in 5s..."
|
echo "workflow.config on zz-ready-to-start not found yet (HTTP Status: $HTTP_STATUS). Retrying in 1s..."
|
||||||
sleep 5
|
sleep 1
|
||||||
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $GITEA_TOKEN" "$API_URL")
|
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $GITEA_TOKEN" "$API_URL")
|
||||||
done
|
done
|
||||||
|
|
||||||
|
echo "workflow.config found on zz-ready-to-start."
|
||||||
|
|
||||||
# Wait for the shared SSH key to be generated by the gitea setup script
|
# Wait for the shared SSH key to be generated by the gitea setup script
|
||||||
echo "Waiting for /var/lib/gitea/ssh-keys/id_ed25519..."
|
echo "Waiting for /var/lib/gitea/ssh-keys/id_ed25519..."
|
||||||
while [ ! -f /var/lib/gitea/ssh-keys/id_ed25519 ]; do
|
while [ ! -f /var/lib/gitea/ssh-keys/id_ed25519 ]; do
|
||||||
@@ -63,4 +66,5 @@ package=$(rpm -qa | grep autogits-workflow-pr) || :
|
|||||||
echo "!!!!!!!!!!!!!!!! using binary $exe; installed package: $package"
|
echo "!!!!!!!!!!!!!!!! using binary $exe; installed package: $package"
|
||||||
which strings > /dev/null 2>&1 && strings "$exe" | grep -A 2 vcs.revision= | head -4 || :
|
which strings > /dev/null 2>&1 && strings "$exe" | grep -A 2 vcs.revision= | head -4 || :
|
||||||
|
|
||||||
|
set -x
|
||||||
exec "$exe" "$@"
|
exec "$exe" "$@"
|
||||||
|
|||||||
@@ -6,5 +6,8 @@
|
|||||||
"myproducts/mySLFO#maintainer-merge",
|
"myproducts/mySLFO#maintainer-merge",
|
||||||
"myproducts/mySLFO#review-required",
|
"myproducts/mySLFO#review-required",
|
||||||
"myproducts/mySLFO#label-test",
|
"myproducts/mySLFO#label-test",
|
||||||
"myproducts/mySLFO#manual-merge"
|
"myproducts/mySLFO#manual-merge",
|
||||||
|
"myproducts/mySLFO#merge-ff",
|
||||||
|
"myproducts/mySLFO#merge-replace",
|
||||||
|
"myproducts/mySLFO#merge-devel"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -131,6 +131,10 @@ func ProcessBuildStatus(project *common.BuildResultList) BuildStatusSummary {
|
|||||||
found:
|
found:
|
||||||
for j := 0; j < len(project.Result); j++ {
|
for j := 0; j < len(project.Result); j++ {
|
||||||
common.LogDebug(" found match for @ idx:", j)
|
common.LogDebug(" found match for @ idx:", j)
|
||||||
|
if project.Result[i].Dirty {
|
||||||
|
// ignore possible temporary failures and wait for settling
|
||||||
|
return BuildStatusSummaryBuilding
|
||||||
|
}
|
||||||
res := ProcessRepoBuildStatus(project.Result[i].Status)
|
res := ProcessRepoBuildStatus(project.Result[i].Status)
|
||||||
switch res {
|
switch res {
|
||||||
case BuildStatusSummarySuccess:
|
case BuildStatusSummarySuccess:
|
||||||
@@ -873,8 +877,11 @@ func ProcessPullRequest(obs common.ObsClientInterface, gitea common.Gitea, org,
|
|||||||
// NOTE: this is user input, so we need some limits here
|
// NOTE: this is user input, so we need some limits here
|
||||||
l := len(stagingConfig.ObsProject)
|
l := len(stagingConfig.ObsProject)
|
||||||
if l >= len(stagingConfig.StagingProject) || stagingConfig.ObsProject != stagingConfig.StagingProject[0:l] {
|
if l >= len(stagingConfig.StagingProject) || stagingConfig.ObsProject != stagingConfig.StagingProject[0:l] {
|
||||||
common.LogError("StagingProject (", stagingConfig.StagingProject, ") is not child of target project", stagingConfig.ObsProject)
|
// TEMPORARY HACK: We remove this when Factory has switched to git
|
||||||
return true, nil
|
if ( stagingConfig.ObsProject != "openSUSE:Factory:git" && stagingConfig.StagingProject != "openSUSE:Factory:PullRequest" ) {
|
||||||
|
common.LogError("StagingProject (", stagingConfig.StagingProject, ") is not child of target project", stagingConfig.ObsProject)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1039,6 +1046,7 @@ func ProcessPullRequest(obs common.ObsClientInterface, gitea common.Gitea, org,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch overallBuildStatus {
|
switch overallBuildStatus {
|
||||||
case BuildStatusSummarySuccess:
|
case BuildStatusSummarySuccess:
|
||||||
status.Status = common.CommitStatus_Success
|
status.Status = common.CommitStatus_Success
|
||||||
@@ -1163,6 +1171,7 @@ var IsDryRun bool
|
|||||||
var ProcessPROnly string
|
var ProcessPROnly string
|
||||||
var ObsClient common.ObsClientInterface
|
var ObsClient common.ObsClientInterface
|
||||||
var BotUser string
|
var BotUser string
|
||||||
|
var PollInterval = 5 * time.Minute
|
||||||
|
|
||||||
func ObsWebHostFromApiHost(apihost string) string {
|
func ObsWebHostFromApiHost(apihost string) string {
|
||||||
u, err := url.Parse(apihost)
|
u, err := url.Parse(apihost)
|
||||||
@@ -1185,9 +1194,18 @@ func main() {
|
|||||||
flag.StringVar(&ObsApiHost, "obs", "", "API for OBS instance")
|
flag.StringVar(&ObsApiHost, "obs", "", "API for OBS instance")
|
||||||
flag.StringVar(&ObsWebHost, "obs-web", "", "Web OBS instance, if not derived from the obs config")
|
flag.StringVar(&ObsWebHost, "obs-web", "", "Web OBS instance, if not derived from the obs config")
|
||||||
flag.BoolVar(&IsDryRun, "dry", false, "Dry-run, don't actually create any build projects or review changes")
|
flag.BoolVar(&IsDryRun, "dry", false, "Dry-run, don't actually create any build projects or review changes")
|
||||||
|
pollIntervalStr := flag.String("poll-interval", common.GetEnvOverrideString(os.Getenv("AUTOGITS_STAGING_BOT_POLL_INTERVAL"), ""), "Polling interval for notifications (e.g. 5m, 10s)")
|
||||||
debug := flag.Bool("debug", false, "Turns on debug logging")
|
debug := flag.Bool("debug", false, "Turns on debug logging")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if len(*pollIntervalStr) > 0 {
|
||||||
|
if d, err := time.ParseDuration(*pollIntervalStr); err == nil {
|
||||||
|
PollInterval = d
|
||||||
|
} else {
|
||||||
|
common.LogError("Invalid poll interval:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if *debug {
|
if *debug {
|
||||||
common.SetLoggingLevel(common.LogLevelDebug)
|
common.SetLoggingLevel(common.LogLevelDebug)
|
||||||
} else {
|
} else {
|
||||||
@@ -1256,6 +1274,6 @@ func main() {
|
|||||||
for {
|
for {
|
||||||
PollWorkNotifications(ObsClient, gitea)
|
PollWorkNotifications(ObsClient, gitea)
|
||||||
common.LogInfo("Poll cycle finished")
|
common.LogInfo("Poll cycle finished")
|
||||||
time.Sleep(5 * time.Minute)
|
time.Sleep(PollInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ This is the ProjectGit config file. For runtime config file, see bottom.
|
|||||||
| *GitProjectName* | Repository and branch where the ProjectGit lives. | no | string | **Format**: `org/project_repo#branch` | By default assumes `_ObsPrj` with default branch in the *Organization* |
|
| *GitProjectName* | Repository and branch where the ProjectGit lives. | no | string | **Format**: `org/project_repo#branch` | By default assumes `_ObsPrj` with default branch in the *Organization* |
|
||||||
| *ManualMergeOnly* | Merges are permitted only upon receiving a "merge ok" comment from designated maintainers in the PkgGit PR. | no | bool | true, false | false |
|
| *ManualMergeOnly* | Merges are permitted only upon receiving a "merge ok" comment from designated maintainers in the PkgGit PR. | no | bool | true, false | false |
|
||||||
| *ManualMergeProject* | Merges are permitted only upon receiving a "merge ok" comment in the ProjectGit PR from project maintainers. | no | bool | true, false | false |
|
| *ManualMergeProject* | Merges are permitted only upon receiving a "merge ok" comment in the ProjectGit PR from project maintainers. | no | bool | true, false | false |
|
||||||
|
| *MergeMode* | Type of package merge accepted. See below for details. | no | string | ff-only, replace, devel | ff-only |
|
||||||
| *ReviewRequired* | If submitter is a maintainer, require review from another maintainer if available. | no | bool | true, false | false |
|
| *ReviewRequired* | If submitter is a maintainer, require review from another maintainer if available. | no | bool | true, false | false |
|
||||||
| *NoProjectGitPR* | Do not create PrjGit PR, but still perform other tasks. | no | bool | true, false | false |
|
| *NoProjectGitPR* | Do not create PrjGit PR, but still perform other tasks. | no | bool | true, false | false |
|
||||||
| *Reviewers* | PrjGit reviewers. Additional review requests are triggered for associated PkgGit PRs. PrjGit PR is merged only when all reviews are complete. | no | array of strings | | `[]` |
|
| *Reviewers* | PrjGit reviewers. Additional review requests are triggered for associated PkgGit PRs. PrjGit PR is merged only when all reviews are complete. | no | array of strings | | `[]` |
|
||||||
@@ -96,6 +97,19 @@ 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.
|
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
|
Labels
|
||||||
------
|
------
|
||||||
|
|
||||||
@@ -104,8 +118,6 @@ The following labels are used, when defined in Repo/Org.
|
|||||||
| Label Config Entry | Default label | Description
|
| Label Config Entry | Default label | Description
|
||||||
|--------------------|----------------|----------------------------------------
|
|--------------------|----------------|----------------------------------------
|
||||||
| StagingAuto | staging/Auto | Assigned to Project Git PRs when first staged
|
| StagingAuto | staging/Auto | Assigned to Project Git PRs when first staged
|
||||||
| ReviewPending | review/Pending | Assigned to Project Git PR when package reviews are still pending
|
|
||||||
| ReviewDone | review/Done | Assigned to Project Git PR when reviews are complete on all package PRs
|
|
||||||
|
|
||||||
|
|
||||||
Maintainership
|
Maintainership
|
||||||
|
|||||||
@@ -417,6 +417,12 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
|
|||||||
}
|
}
|
||||||
common.LogInfo("fetched PRSet of size:", len(prset.PRs))
|
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)
|
prjGitPRbranch := prGitBranchNameForPR(prRepo, prNo)
|
||||||
prjGitPR, err := prset.GetPrjGitPR()
|
prjGitPR, err := prset.GetPrjGitPR()
|
||||||
if err == common.PRSet_PrjGitMissing {
|
if err == common.PRSet_PrjGitMissing {
|
||||||
@@ -470,7 +476,8 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
|
|||||||
if pr.PR.State == "open" {
|
if pr.PR.State == "open" {
|
||||||
org, repo, idx := pr.PRComponents()
|
org, repo, idx := pr.PRComponents()
|
||||||
if prjGitPR.PR.HasMerged {
|
if prjGitPR.PR.HasMerged {
|
||||||
Gitea.AddComment(pr.PR, "This PR is merged via the associated Project PR.")
|
// TODO: use timeline here because this can spam if ManualMergePR fails
|
||||||
|
// Gitea.AddComment(pr.PR, "This PR is merged via the associated Project PR.")
|
||||||
err = Gitea.ManualMergePR(org, repo, idx, pr.PR.Head.Sha, false)
|
err = Gitea.ManualMergePR(org, repo, idx, pr.PR.Head.Sha, false)
|
||||||
if _, ok := err.(*repository.RepoMergePullRequestConflict); !ok {
|
if _, ok := err.(*repository.RepoMergePullRequestConflict); !ok {
|
||||||
common.PanicOnError(err)
|
common.PanicOnError(err)
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ func TestOpenPR(t *testing.T) {
|
|||||||
mockGit.EXPECT().GitClone(gomock.Any(), gomock.Any(), gomock.Any()).Return("origin", nil).AnyTimes()
|
mockGit.EXPECT().GitClone(gomock.Any(), gomock.Any(), gomock.Any()).Return("origin", nil).AnyTimes()
|
||||||
mockGit.EXPECT().GitBranchHead(gomock.Any(), gomock.Any()).Return("head", nil).AnyTimes()
|
mockGit.EXPECT().GitBranchHead(gomock.Any(), gomock.Any()).Return("head", nil).AnyTimes()
|
||||||
mockGit.EXPECT().GitSubmoduleList(gomock.Any(), gomock.Any()).Return(map[string]string{"testRepo": "testing"}, nil).AnyTimes()
|
mockGit.EXPECT().GitSubmoduleList(gomock.Any(), gomock.Any()).Return(map[string]string{"testRepo": "testing"}, nil).AnyTimes()
|
||||||
|
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
mockGit.EXPECT().GitCatFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
|
mockGit.EXPECT().GitCatFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
|
||||||
mockGit.EXPECT().Close().Return(nil).AnyTimes()
|
mockGit.EXPECT().Close().Return(nil).AnyTimes()
|
||||||
@@ -187,6 +188,7 @@ func TestOpenPR(t *testing.T) {
|
|||||||
mockGit.EXPECT().GitClone(gomock.Any(), gomock.Any(), gomock.Any()).Return("origin", nil).AnyTimes()
|
mockGit.EXPECT().GitClone(gomock.Any(), gomock.Any(), gomock.Any()).Return("origin", nil).AnyTimes()
|
||||||
mockGit.EXPECT().GitBranchHead(gomock.Any(), gomock.Any()).Return("head", nil).AnyTimes()
|
mockGit.EXPECT().GitBranchHead(gomock.Any(), gomock.Any()).Return("head", nil).AnyTimes()
|
||||||
mockGit.EXPECT().GitSubmoduleList(gomock.Any(), gomock.Any()).Return(map[string]string{"testRepo": "testing"}, nil).AnyTimes()
|
mockGit.EXPECT().GitSubmoduleList(gomock.Any(), gomock.Any()).Return(map[string]string{"testRepo": "testing"}, nil).AnyTimes()
|
||||||
|
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
mockGit.EXPECT().GitCatFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
|
mockGit.EXPECT().GitCatFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
|
||||||
mockGit.EXPECT().Close().Return(nil).AnyTimes()
|
mockGit.EXPECT().Close().Return(nil).AnyTimes()
|
||||||
@@ -236,6 +238,7 @@ func TestOpenPR(t *testing.T) {
|
|||||||
mockGit.EXPECT().GitClone(gomock.Any(), gomock.Any(), gomock.Any()).Return("origin", nil).AnyTimes()
|
mockGit.EXPECT().GitClone(gomock.Any(), gomock.Any(), gomock.Any()).Return("origin", nil).AnyTimes()
|
||||||
mockGit.EXPECT().GitBranchHead(gomock.Any(), gomock.Any()).Return("head", nil).AnyTimes()
|
mockGit.EXPECT().GitBranchHead(gomock.Any(), gomock.Any()).Return("head", nil).AnyTimes()
|
||||||
mockGit.EXPECT().GitSubmoduleList(gomock.Any(), gomock.Any()).Return(map[string]string{"testRepo": "testing"}, nil).AnyTimes()
|
mockGit.EXPECT().GitSubmoduleList(gomock.Any(), gomock.Any()).Return(map[string]string{"testRepo": "testing"}, nil).AnyTimes()
|
||||||
|
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
mockGit.EXPECT().GitCatFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
|
mockGit.EXPECT().GitCatFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
|
||||||
gitea.EXPECT().RequestReviews(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
|
gitea.EXPECT().RequestReviews(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
|
||||||
@@ -289,6 +292,7 @@ func TestOpenPR(t *testing.T) {
|
|||||||
mockGit.EXPECT().GitClone(gomock.Any(), gomock.Any(), gomock.Any()).Return("origin", nil).AnyTimes()
|
mockGit.EXPECT().GitClone(gomock.Any(), gomock.Any(), gomock.Any()).Return("origin", nil).AnyTimes()
|
||||||
mockGit.EXPECT().GitBranchHead(gomock.Any(), gomock.Any()).Return("head", nil).AnyTimes()
|
mockGit.EXPECT().GitBranchHead(gomock.Any(), gomock.Any()).Return("head", nil).AnyTimes()
|
||||||
mockGit.EXPECT().GitSubmoduleList(gomock.Any(), gomock.Any()).Return(map[string]string{"testRepo": "testing"}, nil).AnyTimes()
|
mockGit.EXPECT().GitSubmoduleList(gomock.Any(), gomock.Any()).Return(map[string]string{"testRepo": "testing"}, nil).AnyTimes()
|
||||||
|
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
mockGit.EXPECT().GitCatFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
|
mockGit.EXPECT().GitCatFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
|
||||||
mockGit.EXPECT().Close().Return(nil).AnyTimes()
|
mockGit.EXPECT().Close().Return(nil).AnyTimes()
|
||||||
@@ -343,6 +347,7 @@ func TestOpenPR(t *testing.T) {
|
|||||||
mockGit.EXPECT().GitClone(gomock.Any(), gomock.Any(), gomock.Any()).Return("origin", nil).AnyTimes()
|
mockGit.EXPECT().GitClone(gomock.Any(), gomock.Any(), gomock.Any()).Return("origin", nil).AnyTimes()
|
||||||
mockGit.EXPECT().GitBranchHead(gomock.Any(), gomock.Any()).Return("head", nil).AnyTimes()
|
mockGit.EXPECT().GitBranchHead(gomock.Any(), gomock.Any()).Return("head", nil).AnyTimes()
|
||||||
mockGit.EXPECT().GitSubmoduleList(gomock.Any(), gomock.Any()).Return(map[string]string{"testRepo": "testing"}, nil).AnyTimes()
|
mockGit.EXPECT().GitSubmoduleList(gomock.Any(), gomock.Any()).Return(map[string]string{"testRepo": "testing"}, nil).AnyTimes()
|
||||||
|
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
mockGit.EXPECT().GitCatFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
|
mockGit.EXPECT().GitCatFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
|
||||||
mockGit.EXPECT().Close().Return(nil).AnyTimes()
|
mockGit.EXPECT().Close().Return(nil).AnyTimes()
|
||||||
|
|||||||
@@ -782,7 +782,10 @@ func TestPRProcessor_Process_EdgeCases(t *testing.T) {
|
|||||||
gitea.EXPECT().FetchMaintainershipFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, "", nil).AnyTimes()
|
gitea.EXPECT().FetchMaintainershipFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, "", nil).AnyTimes()
|
||||||
gitea.EXPECT().FetchMaintainershipDirFile(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, "", nil).AnyTimes()
|
gitea.EXPECT().FetchMaintainershipDirFile(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, "", nil).AnyTimes()
|
||||||
// Mock expectations for the merged path
|
// Mock expectations for the merged path
|
||||||
mockGit.EXPECT().GitClone(gomock.Any(), gomock.Any(), gomock.Any()).Return("origin", nil)
|
mockGit.EXPECT().GitClone(gomock.Any(), gomock.Any(), gomock.Any()).Return("origin", nil).AnyTimes()
|
||||||
|
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
mockGit.EXPECT().GitExec(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||||
mockGit.EXPECT().GitSubmoduleList(gomock.Any(), gomock.Any()).Return(map[string]string{"pkg-a": "old-sha"}, nil).AnyTimes()
|
mockGit.EXPECT().GitSubmoduleList(gomock.Any(), gomock.Any()).Return(map[string]string{"pkg-a": "old-sha"}, nil).AnyTimes()
|
||||||
gitea.EXPECT().GetRecentCommits(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]*models.Commit{{SHA: "pkg-sha"}}, nil).AnyTimes()
|
gitea.EXPECT().GetRecentCommits(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]*models.Commit{{SHA: "pkg-sha"}}, nil).AnyTimes()
|
||||||
|
|
||||||
@@ -950,6 +953,10 @@ func TestProcessFunc(t *testing.T) {
|
|||||||
mockGit.EXPECT().GetPath().Return("/tmp").AnyTimes()
|
mockGit.EXPECT().GetPath().Return("/tmp").AnyTimes()
|
||||||
mockGit.EXPECT().Close().Return(nil)
|
mockGit.EXPECT().Close().Return(nil)
|
||||||
|
|
||||||
|
mockGit.EXPECT().GitClone(gomock.Any(), gomock.Any(), gomock.Any()).Return("origin", nil).AnyTimes()
|
||||||
|
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
|
||||||
// Expect Process calls (mocked via mockGit mostly)
|
// Expect Process calls (mocked via mockGit mostly)
|
||||||
gitea.EXPECT().GetTimeline(gomock.Any(), gomock.Any(), gomock.Any()).Return([]*models.TimelineComment{}, nil).AnyTimes()
|
gitea.EXPECT().GetTimeline(gomock.Any(), gomock.Any(), gomock.Any()).Return([]*models.TimelineComment{}, nil).AnyTimes()
|
||||||
gitea.EXPECT().GetPullRequestReviews(gomock.Any(), gomock.Any(), gomock.Any()).Return([]*models.PullReview{}, nil).AnyTimes()
|
gitea.EXPECT().GetPullRequestReviews(gomock.Any(), gomock.Any(), gomock.Any()).Return([]*models.PullReview{}, nil).AnyTimes()
|
||||||
|
|||||||
@@ -185,6 +185,9 @@ func TestDefaultStateChecker_ProcessPR(t *testing.T) {
|
|||||||
mockGit.EXPECT().GetPath().Return("/tmp").AnyTimes()
|
mockGit.EXPECT().GetPath().Return("/tmp").AnyTimes()
|
||||||
mockGit.EXPECT().Close().Return(nil)
|
mockGit.EXPECT().Close().Return(nil)
|
||||||
|
|
||||||
|
mockGit.EXPECT().GitClone(gomock.Any(), gomock.Any(), gomock.Any()).Return("origin", nil).AnyTimes()
|
||||||
|
mockGit.EXPECT().GitExecOrPanic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
|
||||||
// Expectations for ProcesPullRequest
|
// Expectations for ProcesPullRequest
|
||||||
gitea.EXPECT().GetPullRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(pr, nil).AnyTimes()
|
gitea.EXPECT().GetPullRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(pr, nil).AnyTimes()
|
||||||
gitea.EXPECT().GetTimeline(gomock.Any(), gomock.Any(), gomock.Any()).Return([]*models.TimelineComment{}, nil).AnyTimes()
|
gitea.EXPECT().GetTimeline(gomock.Any(), gomock.Any(), gomock.Any()).Return([]*models.TimelineComment{}, nil).AnyTimes()
|
||||||
|
|||||||
Reference in New Issue
Block a user