17 Commits

Author SHA256 Message Date
91d22f7eea staging: add tests for idempotency and label changes
All checks were successful
go-generate-check / go-generate-check (pull_request) Successful in 33s
We do not want duplicate comments. And if we do have label changes,
new comments should be added.
2026-02-24 18:22:08 +01:00
913b8c8a4b staging: Match previous message format
All checks were successful
go-generate-check / go-generate-check (pull_request) Successful in 28s
Match changes in older message format. That is,

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

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

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

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

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

View File

@@ -1,52 +0,0 @@
name: Integration tests
on:
push:
branches: ['main']
pull_request:
workflow_dispatch:
env:
HOME: /var/lib/gitea-runner
REPO_URL: http://src.opensuse.org//git-workflow/autogits.git
jobs:
t:
runs-on: linux-x86_64
steps:
- name: whoami
run: whoami
- name: pwd
run: pwd
- name: vars
run: |
set | grep GITEA_
- name: Clone
run: |
git clone -q ${{ env.REPO_URL }}
- name: Checkout
run: |
echo ${{ gitea.ref }}
git fetch origin ${{ gitea.ref }}
git checkout FETCH_HEAD
working-directory: ./autogits
- name: Prepare binaries
run: make build
working-directory: ./autogits
- name: Prepare images
run: make build
working-directory: ./autogits/integration
- name: Make sure the pod is down
run: make down
working-directory: ./autogits/integration
- name: Start images
run: make up
working-directory: ./autogits/integration
- name: Run tests
run: py.test-3.11 -v tests
working-directory: ./autogits/integration
- name: Make sure the pod is down
if: always()
run: make down
working-directory: ./autogits/integration

1
.gitignore vendored
View File

@@ -1,6 +1,5 @@
*.osc
*.conf
!/integration/**/*.conf
/integration/gitea-data
/integration/gitea-logs
/integration/rabbitmq-data

View File

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

View File

@@ -46,6 +46,15 @@ type ObsStatusFetcherWithState interface {
BuildStatusWithState(project string, opts *BuildResultOptions, packages ...string) (*BuildResultList, error)
}
type ObsClientInterface interface {
GetProjectMeta(project string) (*ProjectMeta, error)
SetProjectMeta(meta *ProjectMeta) error
DeleteProject(project string) error
BuildStatus(project string, packages ...string) (*BuildResultList, error)
GetHomeProject() string
SetHomeProject(project string)
}
type ObsClient struct {
baseUrl *url.URL
client *http.Client
@@ -57,6 +66,14 @@ type ObsClient struct {
HomeProject string
}
func (c *ObsClient) GetHomeProject() string {
return c.HomeProject
}
func (c *ObsClient) SetHomeProject(project string) {
c.HomeProject = project
}
func NewObsClient(host string) (*ObsClient, error) {
baseUrl, err := url.Parse(host)
if err != nil {

View File

@@ -44,7 +44,7 @@ build_container:
# 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/*"
podman run --rm --privileged -t --network integration_gitea-network -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

View File

@@ -10,7 +10,4 @@ echo "!!!!!!!!!!!!!!!! using binary $exe; installed package: $package"
which strings > /dev/null 2>&1 && strings "$exe" | grep -A 2 vcs.revision= | head -4 || :
echo "RABBITMQ_HOST: $RABBITMQ_HOST"
echo "sleep 12 sec to let rabbitmq set up, because the bot currently retries only once"
sleep 12
exec $exe "$@"

View File

@@ -11,7 +11,7 @@ RUN zypper -n install \
openssh \
jq \
devel_Factory_git-workflow:gitea \
&& rm -rf /var/cache/zypp/* || ( tail -n 1000 /var/log/zypper.log ; exit 1 )
&& rm -rf /var/cache/zypp/*
# Copy the minimal set of required files from the local 'container-files' directory
COPY container-files/ /

View File

@@ -1,7 +0,0 @@
listeners.ssl.default = 5671
ssl_options.certfile = /etc/rabbitmq/certs/cert.pem
ssl_options.keyfile = /etc/rabbitmq/certs/key.pem
ssl_options.verify = verify_none
ssl_options.fail_if_no_peer_cert = false
management.load_definitions = /etc/rabbitmq/definitions.json

View File

@@ -53,37 +53,31 @@ The testing will be conducted in a dedicated test environment that mimics the pr
## 5. Test Cases
| Test Case ID | Status | Description | Steps to Reproduce | Expected Results | Priority |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **TC-SYNC-001** | P | **Create ProjectGit PR from PackageGit PR** | 1. Create a new PR in a PackageGit repository. | 1. A new PR is created in the corresponding ProjectGit repository with the title "Forwarded PRs: <package_name>".<br>2. The ProjectGit PR description contains a link to the PackageGit PR (e.g., `PR: org/package_repo!pr_number`).<br>3. The package submodule in the ProjectGit PR points to the PackageGit PR's commit. | High |
| **TC-SYNC-002** | P | **Update ProjectGit PR from PackageGit PR** | 1. Push a new commit to an existing PackageGit PR. | 1. The corresponding ProjectGit PR's head branch is updated with the new commit. | High |
| **TC-SYNC-003** | P | **WIP Flag Synchronization** | 1. Mark a PackageGit PR as "Work In Progress".<br>2. Remove the WIP flag from the PackageGit PR. | 1. The corresponding ProjectGit PR is also marked as "Work In Progress".<br>2. The WIP flag on the ProjectGit PR is removed. | Medium |
| **TC-SYNC-004** | - | **WIP Flag (multiple referenced package PRs)** | 1. Create a ProjectGit PR that references multiple PackageGit PRs.<br>2. Mark one of the PackageGit PRs as "Work In Progress".<br>3. Remove the "Work In Progress" flag from all PackageGit PRs. | 1. The ProjectGit PR is marked as "Work In Progress".<br>2. The "Work In Progress" flag is removed from the ProjectGit PR only after it has been removed from all associated PackageGit PRs. | Medium |
| **TC-SYNC-005** | x | **NoProjectGitPR = true, edits disabled** | 1. Set `NoProjectGitPR = true` in `workflow.config`.<br>2. Create a PackageGit PR without "Allow edits from maintainers" enabled. <br>3. Push a new commit to the PackageGit PR. | 1. No ProjectGit PR is created.<br>2. The bot adds a warning comment to the PackageGit PR explaining that it cannot update the PR. | High |
| **TC-SYNC-006** | x | **NoProjectGitPR = true, edits enabled** | 1. Set `NoProjectGitPR = true` in `workflow.config`.<br>2. Create a PackageGit PR with "Allow edits from maintainers" enabled.<br>3. Push a new commit to the PackageGit PR. | 1. No ProjectGit PR is created.<br>2. The submodule commit on the project PR is updated with the new commit from the PackageGit PR. | High |
| **TC-COMMENT-001** | - | **Detect duplicate comments** | 1. Create a PackageGit PR.<br>2. Wait for the `workflow-pr` bot to act on the PR.<br>3. Edit the body of the PR to trigger the bot a second time. | 1. The bot should not post a duplicate comment. | High |
| **TC-REVIEW-001** | P | **Add mandatory reviewers** | 1. Create a new PackageGit PR. | 1. All mandatory reviewers are added to both the PackageGit and ProjectGit PRs. | High |
| **TC-REVIEW-002** | - | **Add advisory reviewers** | 1. Create a new PackageGit PR with advisory reviewers defined in the configuration. | 1. Advisory reviewers are added to the PR, but their approval is not required for merging. | Medium |
| **TC-REVIEW-003** | - | **Re-add reviewers** | 1. Push a new commit to a PackageGit PR after it has been approved. | 1. The original reviewers are re-added to the PR. | Medium |
| **TC-REVIEW-004** | x | **Package PR created by a maintainer** | 1. Create a PackageGit PR from the account of a package maintainer. | 1. No review is requested from other package maintainers. | High |
| **TC-REVIEW-005** | P | **Package PR created by an external user (approve)** | 1. Create a PackageGit PR from the account of a user who is not a package maintainer.<br>2. One of the package maintainers approves the PR. | 1. All package maintainers are added as reviewers.<br>2. Once one maintainer approves the PR, the other maintainers are removed as reviewers. | High |
| **TC-REVIEW-006** | P | **Package PR created by an external user (reject)** | 1. Create a PackageGit PR from the account of a user who is not a package maintainer.<br>2. One of the package maintainers rejects the PR. | 1. All package maintainers are added as reviewers.<br>2. Once one maintainer rejects the PR, the other maintainers are removed as reviewers. | High |
| **TC-REVIEW-007** | P | **Package PR created by a maintainer with ReviewRequired=true** | 1. Set `ReviewRequired = true` in `workflow.config`.<br>2. Create a PackageGit PR from the account of a package maintainer. | 1. A review is requested from other package maintainers if available. | High |
| **TC-MERGE-001** | x | **Automatic Merge** | 1. Create a PackageGit PR.<br>2. Ensure all mandatory reviews are completed on both project and package PRs. | 1. The PR is automatically merged. | High |
| **TC-MERGE-002** | - | **ManualMergeOnly with Package Maintainer** | 1. Create a PackageGit PR with `ManualMergeOnly` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on the package PR from the account of a package maintainer for that package. | 1. The PR is merged. | High |
| **TC-MERGE-003** | - | **ManualMergeOnly with unauthorized user** | 1. Create a PackageGit PR with `ManualMergeOnly` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on the package PR from the account of a user who is not a maintainer for that package. | 1. The PR is not merged. | High |
| **TC-MERGE-004** | - | **ManualMergeOnly with multiple packages** | 1. Create a ProjectGit PR that references multiple PackageGit PRs with `ManualMergeOnly` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on each package PR from the account of a package maintainer. | 1. The PR is merged only after "merge ok" is commented on all associated PackageGit PRs. | High |
| **TC-MERGE-005** | - | **ManualMergeOnly with Project Maintainer** | 1. Create a PackageGit PR with `ManualMergeOnly` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on the package PR from the account of a project maintainer. | 1. The PR is merged. | High |
| **TC-MERGE-006** | - | **ManualMergeProject with Project Maintainer** | 1. Create a PackageGit PR with `ManualMergeProject` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on the project PR from the account of a project maintainer. | 1. The PR is merged. | High |
| **TC-MERGE-007** | - | **ManualMergeProject with unauthorized user** | 1. Create a PackageGit PR with `ManualMergeProject` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on the project PR from the account of a package maintainer. | 1. The PR is not merged. | High |
| **TC-CONFIG-001** | - | **Invalid Configuration** | 1. Provide an invalid `workflow.config` file. | 1. The bot reports an error and does not process any PRs. | High |
| **TC-LABEL-001** | P | **Apply `staging/Auto` label** | 1. Create a new PackageGit PR. | 1. The `staging/Auto` label is applied to the ProjectGit PR. | High |
| **TC-LABEL-002** | x | **Apply `review/Pending` label** | 1. Create a new PackageGit PR. | 1. The `review/Pending` label is applied to the ProjectGit PR when there are pending reviews. | Medium |
| **TC-LABEL-003** | - | **Apply `review/Done` label** | 1. Ensure all mandatory reviews for a PR are completed. | 1. The `review/Done` label is applied to the ProjectGit PR when all mandatory reviews are completed. | Medium |
| Test Case ID | Description | Steps to Reproduce | Expected Results | Priority |
| :--- | :--- | :--- | :--- | :--- |
| **TC-SYNC-001** | **Create ProjectGit PR from PackageGit PR** | 1. Create a new PR in a PackageGit repository. | 1. A new PR is created in the corresponding ProjectGit repository with the title "Forwarded PRs: <package_name>".<br>2. The ProjectGit PR description contains a link to the PackageGit PR (e.g., `PR: org/package_repo!pr_number`).<br>3. The package submodule in the ProjectGit PR points to the PackageGit PR's commit. | High |
| **TC-SYNC-002** | **Update ProjectGit PR from PackageGit PR** | 1. Push a new commit to an existing PackageGit PR. | 1. The corresponding ProjectGit PR's head branch is updated with the new commit. | High |
| **TC-SYNC-003** | **WIP Flag Synchronization** | 1. Mark a PackageGit PR as "Work In Progress".<br>2. Remove the WIP flag from the PackageGit PR. | 1. The corresponding ProjectGit PR is also marked as "Work In Progress".<br>2. The WIP flag on the ProjectGit PR is removed. | Medium |
| **TC-SYNC-004** | **WIP Flag (multiple referenced package PRs)** | 1. Create a ProjectGit PR that references multiple PackageGit PRs.<br>2. Mark one of the PackageGit PRs as "Work In Progress".<br>3. Remove the "Work In Progress" flag from all PackageGit PRs. | 1. The ProjectGit PR is marked as "Work In Progress".<br>2. The "Work In Progress" flag is removed from the ProjectGit PR only after it has been removed from all associated PackageGit PRs. | Medium |
| **TC-SYNC-005** | **NoProjectGitPR = true, edits disabled** | 1. Set `NoProjectGitPR = true` in `workflow.config`.<br>2. Create a PackageGit PR without "Allow edits from maintainers" enabled. <br>3. Push a new commit to the PackageGit PR. | 1. No ProjectGit PR is created.<br>2. The bot adds a warning comment to the PackageGit PR explaining that it cannot update the PR. | High |
| **TC-SYNC-006** | **NoProjectGitPR = true, edits enabled** | 1. Set `NoProjectGitPR = true` in `workflow.config`.<br>2. Create a PackageGit PR with "Allow edits from maintainers" enabled.<br>3. Push a new commit to the PackageGit PR. | 1. No ProjectGit PR is created.<br>2. The submodule commit on the project PR is updated with the new commit from the PackageGit PR. | High |
| **TC-COMMENT-001** | **Detect duplicate comments** | 1. Create a PackageGit PR.<br>2. Wait for the `workflow-pr` bot to act on the PR.<br>3. Edit the body of the PR to trigger the bot a second time. | 1. The bot should not post a duplicate comment. | High |
| **TC-REVIEW-001** | **Add mandatory reviewers** | 1. Create a new PackageGit PR. | 1. All mandatory reviewers are added to both the PackageGit and ProjectGit PRs. | High |
| **TC-REVIEW-002** | **Add advisory reviewers** | 1. Create a new PackageGit PR with advisory reviewers defined in the configuration. | 1. Advisory reviewers are added to the PR, but their approval is not required for merging. | Medium |
| **TC-REVIEW-003** | **Re-add reviewers** | 1. Push a new commit to a PackageGit PR after it has been approved. | 1. The original reviewers are re-added to the PR. | Medium |
| **TC-REVIEW-004** | **Package PR created by a maintainer** | 1. Create a PackageGit PR from the account of a package maintainer. | 1. No review is requested from other package maintainers. | High |
| **TC-REVIEW-005** | **Package PR created by an external user (approve)** | 1. Create a PackageGit PR from the account of a user who is not a package maintainer.<br>2. One of the package maintainers approves the PR. | 1. All package maintainers are added as reviewers.<br>2. Once one maintainer approves the PR, the other maintainers are removed as reviewers. | High |
| **TC-REVIEW-006** | **Package PR created by an external user (reject)** | 1. Create a PackageGit PR from the account of a user who is not a package maintainer.<br>2. One of the package maintainers rejects the PR. | 1. All package maintainers are added as reviewers.<br>2. Once one maintainer rejects the PR, the other maintainers are removed as reviewers. | High |
| **TC-REVIEW-007** | **Package PR created by a maintainer with ReviewRequired=true** | 1. Set `ReviewRequired = true` in `workflow.config`.<br>2. Create a PackageGit PR from the account of a package maintainer. | 1. A review is requested from other package maintainers if available. | High |
| **TC-MERGE-001** | **Automatic Merge** | 1. Create a PackageGit PR.<br>2. Ensure all mandatory reviews are completed on both project and package PRs. | 1. The PR is automatically merged. | High |
| **TC-MERGE-002** | **ManualMergeOnly with Package Maintainer** | 1. Create a PackageGit PR with `ManualMergeOnly` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on the package PR from the account of a package maintainer for that package. | 1. The PR is merged. | High |
| **TC-MERGE-003** | **ManualMergeOnly with unauthorized user** | 1. Create a PackageGit PR with `ManualMergeOnly` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on the package PR from the account of a user who is not a maintainer for that package. | 1. The PR is not merged. | High |
| **TC-MERGE-004** | **ManualMergeOnly with multiple packages** | 1. Create a ProjectGit PR that references multiple PackageGit PRs with `ManualMergeOnly` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on each package PR from the account of a package maintainer. | 1. The PR is merged only after "merge ok" is commented on all associated PackageGit PRs. | High |
| **TC-MERGE-005** | **ManualMergeOnly with Project Maintainer** | 1. Create a PackageGit PR with `ManualMergeOnly` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on the package PR from the account of a project maintainer. | 1. The PR is merged. | High |
| **TC-MERGE-006** | **ManualMergeProject with Project Maintainer** | 1. Create a PackageGit PR with `ManualMergeProject` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on the project PR from the account of a project maintainer. | 1. The PR is merged. | High |
| **TC-MERGE-007** | **ManualMergeProject with unauthorized user** | 1. Create a PackageGit PR with `ManualMergeProject` set to `true`.<br>2. Ensure all mandatory reviews are completed on both project and package PRs.<br>3. Comment "merge ok" on the project PR from the account of a package maintainer. | 1. The PR is not merged. | High |
| **TC-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** | **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** | **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-003** | **Apply `review/Done` label** | 1. Ensure all mandatory reviews for a PR are completed. | 1. The `review/Done` label is applied to the ProjectGit PR when all mandatory reviews are completed. | Medium |
#### Legend:
* P = implemented and passing;
* x = likely implemented, but investigation is needed;
* X = implemented and likely to pass, but someteimes may fail, but troubleshooting is needed;
* - = test is not implemented

View File

@@ -6,77 +6,232 @@ import pytest
import requests
import time
import os
import json
import base64
# Assuming GiteaAPIClient is in tests/lib/common_test_utils.py
from tests.lib.common_test_utils import GiteaAPIClient
BRANCH_CONFIG_COMMON = {
"workflow.config": {
@pytest.fixture(scope="session")
def gitea_env():
"""
Sets up the Gitea environment with dummy data and provides a GiteaAPIClient instance.
"""
gitea_url = "http://127.0.0.1:3000"
# Read admin token
admin_token_path = "./gitea-data/admin.token" # Corrected path
admin_token = None
try:
with open(admin_token_path, "r") as f:
admin_token = f.read().strip()
except FileNotFoundError:
raise Exception(f"Admin token file not found at {admin_token_path}. Ensure it's generated and accessible.")
# Headers for authenticated requests
auth_headers = {"Authorization": f"token {admin_token}", "Content-Type": "application/json"}
# Wait for Gitea to be available
print(f"Waiting for Gitea at {gitea_url}...")
max_retries = 5
for i in range(max_retries):
try:
# Check a specific API endpoint that indicates readiness
response = requests.get(f"{gitea_url}/api/v1/version", headers=auth_headers, timeout=5)
if response.status_code == 200:
print("Gitea API is available.")
break
except requests.exceptions.ConnectionError:
pass
print(f"Gitea not ready ({response.status_code if 'response' in locals() else 'ConnectionError'}), retrying in 1 seconds... ({i+1}/{max_retries})")
time.sleep(1)
else:
raise Exception("Gitea did not become available within the expected time.")
client = GiteaAPIClient(base_url=gitea_url, token=admin_token)
# Setup dummy data
print("--- Starting Gitea Dummy Data Setup from Pytest Fixture ---")
client.create_org("products")
client.create_org("pool")
client.create_repo("products", "SLFO")
client.create_repo("pool", "pkgA")
client.create_repo("pool", "pkgB")
# The add_submodules method also creates workflow.config and staging.config
client.add_submodules("products", "SLFO")
time.sleep(1)
workflow_config_content = """{
"Workflows": ["pr"],
"GitProjectName": "products/SLFO#main",
"Organization": "pool",
"Reviewers": ["-autogits_obs_staging_bot"],
"GitProjectName": "products/SLFO#{branch}"
},
"_maintainership.json": {
"": ["ownerX", "ownerY"],
"pkgA": ["ownerA"],
"pkgB": ["ownerB", "ownerBB"]
}
}
"Branch": "main",
"ManualMergeProject": true,
"Reviewers": [ "-autogits_obs_staging_bot" ]
}"""
client.create_file("products", "SLFO", "workflow.config", workflow_config_content)
BRANCH_CONFIG_CUSTOM = {
"main": {
"workflow.config": {
"ManualMergeProject": True
},
"staging.config": {
"ObsProject": "openSUSE:Leap:16.0",
"StagingProject": "openSUSE:Leap:16.0:PullRequest"
}
},
"merge": {
"workflow.config": {
"Reviewers": ["+usera", "+userb", "-autogits_obs_staging_bot"]
}
},
"maintainer-merge": {
"workflow.config": {
}
},
"review-required": {
"workflow.config": {
"ReviewRequired": True
}
},
"dev": {
"workflow.config": {
"ManualMergeProject": True,
"NoProjectGitPR": True
}
},
"label-test": {
"workflow.config": {
"ManualMergeProject": True,
"Reviewers": ["*usera"],
"ReviewRequired": True,
"Labels": {
"StagingAuto": "staging/Backlog",
"ReviewPending": "review/Pending"
}
}
}
}
staging_config_content = """{
"ObsProject": "openSUSE:Leap:16.0",
"StagingProject": "openSUSE:Leap:16.0:PullRequest"
}"""
client.create_file("products", "SLFO", "staging.config", staging_config_content)
def setup_users_from_config(client: GiteaAPIClient, wf: dict, mt: dict):
client.add_collaborator("products", "SLFO", "autogits_obs_staging_bot", "write")
client.add_collaborator("products", "SLFO", "workflow-pr", "write")
client.add_collaborator("pool", "pkgA", "workflow-pr", "write")
client.add_collaborator("pool", "pkgB", "workflow-pr", "write")
client.update_repo_settings("products", "SLFO")
client.update_repo_settings("pool", "pkgA")
client.update_repo_settings("pool", "pkgB")
print("--- Gitea Dummy Data Setup Complete ---")
time.sleep(1) # Give workflow-pr bot time to become fully active
yield client
@pytest.fixture(scope="session")
def configured_dev_branch_env(gitea_env: GiteaAPIClient, request):
"""
Fixture to set up a 'dev' branch in products/SLFO and pool/pkgA,
and configure workflow.config in products/SLFO#dev with specific content.
Yields (gitea_env, test_full_repo_name, dev_branch_name).
"""
test_org_name = "products"
test_repo_name = "SLFO"
test_full_repo_name = f"{test_org_name}/{test_repo_name}"
dev_branch_name = "dev"
workflow_config_content = request.param # Get config content from parametrization
print(f"--- Setting up 'dev' branch and workflow.config in {test_full_repo_name}#{dev_branch_name} ---")
# Get the latest commit SHA of the main branch
main_branch_sha = gitea_env._request("GET", f"repos/{test_org_name}/{test_repo_name}/branches/main").json()["commit"]["id"]
# Create 'dev' branch from 'main' in products/SLFO
gitea_env.create_branch(test_org_name, test_repo_name, dev_branch_name, main_branch_sha)
# Create 'dev' branch in pool/pkgA as well
pool_pkga_main_sha = gitea_env._request("GET", "repos/pool/pkgA/branches/main").json()["commit"]["id"]
gitea_env.create_branch("pool", "pkgA", dev_branch_name, pool_pkga_main_sha)
# Create 'dev' branch in pool/pkgB as well
pool_pkgb_main_sha = gitea_env._request("GET", "repos/pool/pkgB/branches/main").json()["commit"]["id"]
gitea_env.create_branch("pool", "pkgB", dev_branch_name, pool_pkgb_main_sha)
# Create/update workflow.config with the provided content
gitea_env.create_file(test_org_name, test_repo_name, "workflow.config", workflow_config_content, branch=dev_branch_name)
print(f"Created workflow.config with specific content in {test_full_repo_name}#{dev_branch_name}")
# Restart workflow-pr service to pick up new project config
gitea_env.restart_service("workflow-pr")
time.sleep(1) # Give the service time to restart and re-initialize
yield gitea_env, test_full_repo_name, dev_branch_name
# Teardown (optional, depending on test strategy)
# For now, we'll leave resources for inspection. If a clean slate is needed for each test,
# this fixture's scope would be 'function' and teardown logic would be added here.
@pytest.fixture(scope="session")
def no_project_git_pr_env(gitea_env: GiteaAPIClient):
"""
Sets up 'dev' branch in products/SLFO and pool/pkgA,
and configures workflow.config in products/SLFO#dev with NoProjectGitPR: true.
"""
test_org_name = "products"
test_repo_name = "SLFO"
test_full_repo_name = f"{test_org_name}/{test_repo_name}"
dev_branch_name = "dev"
print(f"--- Setting up workflow.config in {test_full_repo_name}#{dev_branch_name} for No Project PR ---")
# Get the latest commit SHA of the main branch
main_branch_sha = gitea_env._request("GET", f"repos/{test_org_name}/{test_repo_name}/branches/main").json()["commit"]["id"]
# Create 'dev' branch from 'main' in products/SLFO
try:
gitea_env.create_branch(test_org_name, test_repo_name, dev_branch_name, main_branch_sha)
except Exception as e:
if "already exists" not in str(e).lower():
raise
# Create 'dev' branch in pool/pkgA as well
pool_pkga_main_sha = gitea_env._request("GET", "repos/pool/pkgA/branches/main").json()["commit"]["id"]
try:
gitea_env.create_branch("pool", "pkgA", dev_branch_name, pool_pkga_main_sha)
except Exception as e:
if "already exists" not in str(e).lower():
raise
# Create 'dev' branch in pool/pkgB as well
pool_pkgb_main_sha = gitea_env._request("GET", "repos/pool/pkgB/branches/main").json()["commit"]["id"]
try:
gitea_env.create_branch("pool", "pkgB", dev_branch_name, pool_pkgb_main_sha)
except Exception as e:
if "already exists" not in str(e).lower():
raise
# Setup workflow.config to have "NoProjectGitPR": true
workflow_config_content_no_project_pr = f"""{{
"Workflows": ["pr"],
"GitProjectName": "{test_full_repo_name}#{dev_branch_name}",
"Organization": "pool",
"Branch": "dev",
"ManualMergeProject": true,
"Reviewers": [ "-autogits_obs_staging_bot" ],
"NoProjectGitPR": true
}}"""
gitea_env.create_file(test_org_name, test_repo_name, "workflow.config", workflow_config_content_no_project_pr, branch=dev_branch_name)
print(f"Created workflow.config with NoProjectGitPR: true in {test_full_repo_name}#{dev_branch_name}")
# Restart workflow-pr service
gitea_env.restart_service("workflow-pr")
time.sleep(1) # Give the service time to restart and re-initialize
return gitea_env, test_full_repo_name, dev_branch_name
@pytest.fixture(scope="session")
def test_user_client(gitea_env: GiteaAPIClient):
"""
Creates a new unique user and returns a GiteaAPIClient instance for them using sudo.
This user should not have write permissions to the test repositories by default.
"""
username = f"user-{int(time.time())}"
password = "password123"
email = f"{username}@example.com"
gitea_env.create_user(username, password, email)
# Grant write access to pool/pkgA
gitea_env.add_collaborator("pool", "pkgA", username, "write")
# Use admin token with Sudo header
admin_token = gitea_env.headers["Authorization"].split(" ")[1]
return GiteaAPIClient(base_url=gitea_env.base_url, token=admin_token, sudo=username)
def setup_users_from_config(client: GiteaAPIClient, workflow_config: str, maintainership_config: str):
"""
Parses workflow.config and _maintainership.json, creates users, and adds them as collaborators.
"""
import json
wf = json.loads(workflow_config)
mt = json.loads(maintainership_config)
all_users = set()
# Extract from workflow.config Reviewers
reviewers = wf.get("Reviewers", [])
for r in reviewers:
username = r.lstrip("+-*")
# Strip +, - prefixes
username = r.lstrip("+-")
if username and username not in ["autogits_obs_staging_bot", "workflow-pr"]:
all_users.add(username)
@@ -88,6 +243,8 @@ def setup_users_from_config(client: GiteaAPIClient, wf: dict, mt: dict):
# Create all users
for username in all_users:
client.create_user(username, "password123", f"{username}@example.com")
# Global maintainers (empty key) get write access to everything
# Actually, let's just make them collaborators on SLFO, pkgA, pkgB for simplicity in tests
client.add_collaborator("products", "SLFO", username, "write")
# Set specific repository permissions based on maintainership
@@ -95,167 +252,469 @@ def setup_users_from_config(client: GiteaAPIClient, wf: dict, mt: dict):
repo_name = pkg if pkg else None
for username in users:
if not repo_name:
# Global maintainer - already added to SLFO, add to pkgA/pkgB
client.add_collaborator("pool", "pkgA", username, "write")
client.add_collaborator("pool", "pkgB", username, "write")
else:
client.add_collaborator("pool", repo_name, username, "write")
def ensure_config_file(client: GiteaAPIClient, owner: str, repo: str, branch: str, file_name: str, expected_content_dict: dict):
"""
Checks if a config file exists and has the correct content.
Returns True if a change was made, False otherwise.
"""
file_info = client.get_file_info(owner, repo, file_name, branch=branch)
expected_content = json.dumps(expected_content_dict, indent=4)
if file_info:
current_content_raw = base64.b64decode(file_info["content"]).decode("utf-8")
try:
current_content_dict = json.loads(current_content_raw)
if current_content_dict == expected_content_dict:
return False
except json.JSONDecodeError:
pass # Overwrite invalid JSON
client.create_file(owner, repo, file_name, expected_content, branch=branch)
return True
@pytest.fixture(scope="session")
def gitea_env():
"""
Global fixture to set up the Gitea environment for all tests.
Sets up the Gitea environment with dummy data and provides a GiteaAPIClient instance.
"""
gitea_url = "http://127.0.0.1:3000"
admin_token_path = "./gitea-data/admin.token"
# Read admin token
admin_token_path = "./gitea-data/admin.token" # Corrected path
admin_token = None
try:
with open(admin_token_path, "r") as f:
admin_token = f.read().strip()
except FileNotFoundError:
raise Exception(f"Admin token file not found at {admin_token_path}.")
raise Exception(f"Admin token file not found at {admin_token_path}. Ensure it's generated and accessible.")
# Headers for authenticated requests
auth_headers = {"Authorization": f"token {admin_token}", "Content-Type": "application/json"}
# Wait for Gitea to be available
print(f"Waiting for Gitea at {gitea_url}...")
max_retries = 5
for i in range(max_retries):
try:
# Check a specific API endpoint that indicates readiness
response = requests.get(f"{gitea_url}/api/v1/version", headers=auth_headers, timeout=5)
if response.status_code == 200:
print("Gitea API is available.")
break
except requests.exceptions.ConnectionError:
pass
print(f"Gitea not ready ({response.status_code if 'response' in locals() else 'ConnectionError'}), retrying in 1 seconds... ({i+1}/{max_retries})")
time.sleep(1)
else:
raise Exception("Gitea did not become available within the expected time.")
client = GiteaAPIClient(base_url=gitea_url, token=admin_token)
# Wait for Gitea
for i in range(10):
try:
if client._request("GET", "version").status_code == 200:
break
except:
pass
time.sleep(1)
else:
raise Exception("Gitea not available.")
print("--- Starting Gitea Global Setup ---")
# Setup dummy data
print("--- Starting Gitea Dummy Data Setup from Pytest Fixture ---")
client.create_org("products")
client.create_org("pool")
client.create_repo("products", "SLFO")
client.create_repo("pool", "pkgA")
client.create_repo("pool", "pkgB")
client.update_repo_settings("products", "SLFO")
client.update_repo_settings("pool", "pkgA")
client.update_repo_settings("pool", "pkgB")
# Create labels
client.create_label("products", "SLFO", "staging/Backlog", color="#0000ff")
client.create_label("products", "SLFO", "review/Pending", color="#ffff00")
# Submodules in SLFO
# The add_submodules method also creates workflow.config and staging.config
client.add_submodules("products", "SLFO")
time.sleep(1)
workflow_config_content = """{
"Workflows": ["pr"],
"GitProjectName": "products/SLFO#main",
"Organization": "pool",
"Branch": "main",
"ManualMergeProject": true,
"Reviewers": [ "-autogits_obs_staging_bot" ]
}"""
client.create_file("products", "SLFO", "workflow.config", workflow_config_content)
staging_config_content = """{
"ObsProject": "openSUSE:Leap:16.0",
"StagingProject": "openSUSE:Leap:16.0:PullRequest"
}"""
client.create_file("products", "SLFO", "staging.config", staging_config_content)
maintainership_content = """{
"": ["ownerX","ownerY"],
"pkgA": ["ownerA"],
"pkgB": ["ownerB","ownerBB"]
}"""
# Create users from default main config
setup_users_from_config(client, workflow_config_content, maintainership_content)
client.add_collaborator("products", "SLFO", "autogits_obs_staging_bot", "write")
client.add_collaborator("products", "SLFO", "workflow-pr", "write")
client.add_collaborator("pool", "pkgA", "workflow-pr", "write")
client.add_collaborator("pool", "pkgB", "workflow-pr", "write")
restart_needed = False
# Setup all branches and configs
for branch_name, custom_configs in BRANCH_CONFIG_CUSTOM.items():
# Ensure branch exists in all 3 repos
for owner, repo in [("products", "SLFO"), ("pool", "pkgA"), ("pool", "pkgB")]:
if branch_name != "main":
try:
main_sha = client._request("GET", f"repos/{owner}/{repo}/branches/main").json()["commit"]["id"]
client.create_branch(owner, repo, branch_name, main_sha)
except Exception as e:
if "already exists" not in str(e).lower():
raise
# Merge configs
merged_configs = {}
for file_name, common_content in BRANCH_CONFIG_COMMON.items():
merged_configs[file_name] = common_content.copy()
# Dynamically format values containing {branch}
if file_name == "workflow.config":
if "GitProjectName" in merged_configs[file_name]:
merged_configs[file_name]["GitProjectName"] = merged_configs[file_name]["GitProjectName"].format(branch=branch_name)
# Inject branch name dynamically
merged_configs[file_name]["Branch"] = branch_name
for file_name, custom_content in custom_configs.items():
if file_name in merged_configs:
merged_configs[file_name].update(custom_content)
else:
merged_configs[file_name] = custom_content
client.update_repo_settings("products", "SLFO")
client.update_repo_settings("pool", "pkgA")
client.update_repo_settings("pool", "pkgB")
print("--- Gitea Dummy Data Setup Complete ---")
time.sleep(1) # Give workflow-pr bot time to become fully active
# Ensure config files in products/SLFO
for file_name, content_dict in merged_configs.items():
if ensure_config_file(client, "products", "SLFO", branch_name, file_name, content_dict):
restart_needed = True
# Setup users (using configs from this branch)
setup_users_from_config(client, merged_configs.get("workflow.config", {}), merged_configs.get("_maintainership.json", {}))
if restart_needed:
client.restart_service("workflow-pr")
time.sleep(2) # Give it time to pick up changes
print("--- Gitea Global Setup Complete ---")
yield client
@pytest.fixture(scope="session")
def automerge_env(gitea_env):
return gitea_env, "products/SLFO", "merge"
@pytest.fixture(scope="session")
def maintainer_env(gitea_env):
return gitea_env, "products/SLFO", "maintainer-merge"
def configured_dev_branch_env(gitea_env: GiteaAPIClient, request):
"""
Fixture to set up a 'dev' branch in products/SLFO and pool/pkgA,
and configure workflow.config in products/SLFO#dev with specific content.
Yields (gitea_env, test_full_repo_name, dev_branch_name).
"""
test_org_name = "products"
test_repo_name = "SLFO"
test_full_repo_name = f"{test_org_name}/{test_repo_name}"
dev_branch_name = "dev"
workflow_config_content = request.param # Get config content from parametrization
print(f"--- Setting up 'dev' branch and workflow.config in {test_full_repo_name}#{dev_branch_name} ---")
# Get the latest commit SHA of the main branch
gitea_env.ensure_branch_exists(test_org_name, test_repo_name, "main")
main_branch_sha = gitea_env._request("GET", f"repos/{test_org_name}/{test_repo_name}/branches/main").json()["commit"]["id"]
# Create 'dev' branch from 'main' in products/SLFO
gitea_env.create_branch(test_org_name, test_repo_name, dev_branch_name, main_branch_sha)
# Create 'dev' branch in pool/pkgA as well
gitea_env.ensure_branch_exists("pool", "pkgA", "main")
pool_pkga_main_sha = gitea_env._request("GET", "repos/pool/pkgA/branches/main").json()["commit"]["id"]
gitea_env.create_branch("pool", "pkgA", dev_branch_name, pool_pkga_main_sha)
# Create 'dev' branch in pool/pkgB as well
gitea_env.ensure_branch_exists("pool", "pkgB", "main")
pool_pkgb_main_sha = gitea_env._request("GET", "repos/pool/pkgB/branches/main").json()["commit"]["id"]
gitea_env.create_branch("pool", "pkgB", dev_branch_name, pool_pkgb_main_sha)
# Create/update workflow.config with the provided content
gitea_env.create_file(test_org_name, test_repo_name, "workflow.config", workflow_config_content, branch=dev_branch_name)
# For this fixture, we use default maintainership as we don't receive it in request.param
maintainership_content = """{
"": ["ownerX","ownerY"],
"pkgA": ["ownerA"],
"pkgB": ["ownerB","ownerBB"]
}"""
setup_users_from_config(gitea_env, workflow_config_content, maintainership_content)
print(f"Created workflow.config with specific content in {test_full_repo_name}#{dev_branch_name}")
# Restart workflow-pr service to pick up new project config
gitea_env.restart_service("workflow-pr")
time.sleep(1) # Give the service time to restart and re-initialize
yield gitea_env, test_full_repo_name, dev_branch_name
@pytest.fixture(scope="session")
def review_required_env(gitea_env):
return gitea_env, "products/SLFO", "review-required"
def no_project_git_pr_env(gitea_env: GiteaAPIClient):
"""
Sets up 'dev' branch in products/SLFO and pool/pkgA,
and configures workflow.config in products/SLFO#dev with NoProjectGitPR: true.
"""
test_org_name = "products"
test_repo_name = "SLFO"
test_full_repo_name = f"{test_org_name}/{test_repo_name}"
dev_branch_name = "dev"
print(f"--- Setting up workflow.config in {test_full_repo_name}#{dev_branch_name} for No Project PR ---")
# Get the latest commit SHA of the main branch
gitea_env.ensure_branch_exists(test_org_name, test_repo_name, "main")
main_branch_sha = gitea_env._request("GET", f"repos/{test_org_name}/{test_repo_name}/branches/main").json()["commit"]["id"]
# Create 'dev' branch from 'main' in products/SLFO
try:
gitea_env.create_branch(test_org_name, test_repo_name, dev_branch_name, main_branch_sha)
except Exception as e:
if "already exists" not in str(e).lower():
raise
# Create 'dev' branch in pool/pkgA as well
gitea_env.ensure_branch_exists("pool", "pkgA", "main")
pool_pkga_main_sha = gitea_env._request("GET", "repos/pool/pkgA/branches/main").json()["commit"]["id"]
try:
gitea_env.create_branch("pool", "pkgA", dev_branch_name, pool_pkga_main_sha)
except Exception as e:
if "already exists" not in str(e).lower():
raise
# Create 'dev' branch in pool/pkgB as well
gitea_env.ensure_branch_exists("pool", "pkgB", "main")
pool_pkgb_main_sha = gitea_env._request("GET", "repos/pool/pkgB/branches/main").json()["commit"]["id"]
try:
gitea_env.create_branch("pool", "pkgB", dev_branch_name, pool_pkgb_main_sha)
except Exception as e:
if "already exists" not in str(e).lower():
raise
# Setup workflow.config to have "NoProjectGitPR": true
workflow_config_content = f"""{{
"Workflows": ["pr"],
"GitProjectName": "{test_full_repo_name}#{dev_branch_name}",
"Organization": "pool",
"Branch": "dev",
"ManualMergeProject": true,
"Reviewers": [ "-autogits_obs_staging_bot" ],
"NoProjectGitPR": true
}}"""
gitea_env.create_file(test_org_name, test_repo_name, "workflow.config", workflow_config_content, branch=dev_branch_name)
maintainership_content = """{
"": ["ownerX","ownerY"],
"pkgA": ["ownerA"],
"pkgB": ["ownerB","ownerBB"]
}"""
setup_users_from_config(gitea_env, workflow_config_content, maintainership_content)
print(f"Created workflow.config with NoProjectGitPR: true in {test_full_repo_name}#{dev_branch_name}")
# Restart workflow-pr service
gitea_env.restart_service("workflow-pr")
time.sleep(1) # Give the service time to restart and re-initialize
return gitea_env, test_full_repo_name, dev_branch_name
@pytest.fixture(scope="session")
def no_project_git_pr_env(gitea_env):
return gitea_env, "products/SLFO", "dev"
@pytest.fixture(scope="session")
def label_env(gitea_env):
return gitea_env, "products/SLFO", "label-test"
@pytest.fixture(scope="session")
def ownerA_client(gitea_env):
return GiteaAPIClient(base_url=gitea_env.base_url, token=gitea_env.headers["Authorization"].split(" ")[1], sudo="ownerA")
@pytest.fixture(scope="session")
def ownerB_client(gitea_env):
return GiteaAPIClient(base_url=gitea_env.base_url, token=gitea_env.headers["Authorization"].split(" ")[1], sudo="ownerB")
@pytest.fixture(scope="session")
def ownerBB_client(gitea_env):
return GiteaAPIClient(base_url=gitea_env.base_url, token=gitea_env.headers["Authorization"].split(" ")[1], sudo="ownerBB")
@pytest.fixture(scope="session")
def staging_bot_client(gitea_env):
return GiteaAPIClient(base_url=gitea_env.base_url, token=gitea_env.headers["Authorization"].split(" ")[1], sudo="autogits_obs_staging_bot")
@pytest.fixture(scope="session")
def test_user_client(gitea_env):
username = f"test-user-{int(time.time())}"
gitea_env.create_user(username, "password123", f"{username}@example.com")
def test_user_client(gitea_env: GiteaAPIClient):
"""
Creates a new unique user and returns a GiteaAPIClient instance for them using sudo.
This user should not have write permissions to the test repositories by default.
"""
username = f"user-{int(time.time())}"
password = "password123"
email = f"{username}@example.com"
gitea_env.create_user(username, password, email)
# Grant write access to pool/pkgA
gitea_env.add_collaborator("pool", "pkgA", username, "write")
gitea_env.add_collaborator("products", "SLFO", username, "write")
return GiteaAPIClient(base_url=gitea_env.base_url, token=gitea_env.headers["Authorization"].split(" ")[1], sudo=username)
# Use admin token with Sudo header
admin_token = gitea_env.headers["Authorization"].split(" ")[1]
return GiteaAPIClient(base_url=gitea_env.base_url, token=admin_token, sudo=username)
@pytest.fixture(scope="session")
def automerge_env(gitea_env: GiteaAPIClient):
"""
Sets up 'merge' branch and custom workflow.config for automerge tests.
"""
test_org_name = "products"
test_repo_name = "SLFO"
test_full_repo_name = f"{test_org_name}/{test_repo_name}"
merge_branch_name = "merge"
print(f"--- Setting up '{merge_branch_name}' branch and workflow.config in {test_full_repo_name} ---")
# Get the latest commit SHA of the main branch
gitea_env.ensure_branch_exists(test_org_name, test_repo_name, "main")
main_branch_sha = gitea_env._request("GET", f"repos/{test_org_name}/{test_repo_name}/branches/main").json()["commit"]["id"]
# Create 'merge' branch from 'main' in products/SLFO
try:
gitea_env.create_branch(test_org_name, test_repo_name, merge_branch_name, main_branch_sha)
except Exception as e:
if "already exists" not in str(e).lower():
raise
# Create 'merge' branch in pool/pkgA as well
gitea_env.ensure_branch_exists("pool", "pkgA", "main")
pool_pkga_main_sha = gitea_env._request("GET", "repos/pool/pkgA/branches/main").json()["commit"]["id"]
try:
gitea_env.create_branch("pool", "pkgA", merge_branch_name, pool_pkga_main_sha)
except Exception as e:
if "already exists" not in str(e).lower():
raise
# Create 'merge' branch in pool/pkgB as well
gitea_env.ensure_branch_exists("pool", "pkgB", "main")
pool_pkgb_main_sha = gitea_env._request("GET", "repos/pool/pkgB/branches/main").json()["commit"]["id"]
try:
gitea_env.create_branch("pool", "pkgB", merge_branch_name, pool_pkgb_main_sha)
except Exception as e:
if "already exists" not in str(e).lower():
raise
custom_workflow_config = f"""{{
"Workflows": ["pr"],
"GitProjectName": "{test_full_repo_name}#{merge_branch_name}",
"Organization": "pool",
"Branch": "{merge_branch_name}",
"Reviewers": [ "+usera", "+userb", "-autogits_obs_staging_bot" ]
}}"""
gitea_env.create_file(test_org_name, test_repo_name, "workflow.config", custom_workflow_config, branch=merge_branch_name)
maintainership_content = """{
"": ["ownerX","ownerY"],
"pkgA": ["ownerA"],
"pkgB": ["ownerB","ownerBB"]
}"""
gitea_env.create_file(test_org_name, test_repo_name, "_maintainership.json", maintainership_content, branch=merge_branch_name)
setup_users_from_config(gitea_env, custom_workflow_config, maintainership_content)
# Restart workflow-pr service
gitea_env.restart_service("workflow-pr")
time.sleep(1)
return gitea_env, test_full_repo_name, merge_branch_name
@pytest.fixture(scope="session")
def maintainer_env(gitea_env: GiteaAPIClient):
"""
Sets up 'maintainer-merge' branch and workflow.config without mandatory reviewers.
"""
test_org_name = "products"
test_repo_name = "SLFO"
test_full_repo_name = f"{test_org_name}/{test_repo_name}"
branch_name = "maintainer-merge"
print(f"--- Setting up '{branch_name}' branch and workflow.config in {test_full_repo_name} ---")
# Get the latest commit SHA of the main branch
gitea_env.ensure_branch_exists(test_org_name, test_repo_name, "main")
main_branch_sha = gitea_env._request("GET", f"repos/{test_org_name}/{test_repo_name}/branches/main").json()["commit"]["id"]
# Create branch in products/SLFO
try:
gitea_env.create_branch(test_org_name, test_repo_name, branch_name, main_branch_sha)
except Exception as e:
if "already exists" not in str(e).lower():
raise
# Create branch in pool/pkgA
gitea_env.ensure_branch_exists("pool", "pkgA", "main")
pool_pkga_main_sha = gitea_env._request("GET", "repos/pool/pkgA/branches/main").json()["commit"]["id"]
try:
gitea_env.create_branch("pool", "pkgA", branch_name, pool_pkga_main_sha)
except Exception as e:
if "already exists" not in str(e).lower():
raise
# Create branch in pool/pkgB
gitea_env.ensure_branch_exists("pool", "pkgB", "main")
pool_pkgb_main_sha = gitea_env._request("GET", "repos/pool/pkgB/branches/main").json()["commit"]["id"]
try:
gitea_env.create_branch("pool", "pkgB", branch_name, pool_pkgb_main_sha)
except Exception as e:
if "already exists" not in str(e).lower():
raise
custom_workflow_config = f"""{{
"Workflows": ["pr"],
"GitProjectName": "{test_full_repo_name}#{branch_name}",
"Organization": "pool",
"Branch": "{branch_name}",
"Reviewers": [ "-autogits_obs_staging_bot" ]
}}"""
gitea_env.create_file(test_org_name, test_repo_name, "workflow.config", custom_workflow_config, branch=branch_name)
maintainership_content = """{
"": ["ownerX","ownerY"],
"pkgA": ["ownerA"],
"pkgB": ["ownerB","ownerBB"]
}"""
gitea_env.create_file(test_org_name, test_repo_name, "_maintainership.json", maintainership_content, branch=branch_name)
setup_users_from_config(gitea_env, custom_workflow_config, maintainership_content)
gitea_env.add_collaborator(test_org_name, test_repo_name, "autogits_obs_staging_bot", "write")
# Restart workflow-pr service
gitea_env.restart_service("workflow-pr")
time.sleep(1)
return gitea_env, test_full_repo_name, branch_name
@pytest.fixture(scope="session")
def review_required_env(gitea_env: GiteaAPIClient):
"""
Sets up 'review-required' branch and workflow.config with ReviewRequired: true.
"""
test_org_name = "products"
test_repo_name = "SLFO"
test_full_repo_name = f"{test_org_name}/{test_repo_name}"
branch_name = "review-required"
print(f"--- Setting up '{branch_name}' branch and workflow.config in {test_full_repo_name} ---")
# Get the latest commit SHA of the main branch
gitea_env.ensure_branch_exists(test_org_name, test_repo_name, "main")
main_branch_sha = gitea_env._request("GET", f"repos/{test_org_name}/{test_repo_name}/branches/main").json()["commit"]["id"]
# Create branch in products/SLFO
try:
gitea_env.create_branch(test_org_name, test_repo_name, branch_name, main_branch_sha)
except Exception as e:
if "already exists" not in str(e).lower():
raise
# Create branch in pool/pkgA
gitea_env.ensure_branch_exists("pool", "pkgA", "main")
pool_pkga_main_sha = gitea_env._request("GET", "repos/pool/pkgA/branches/main").json()["commit"]["id"]
try:
gitea_env.create_branch("pool", "pkgA", branch_name, pool_pkga_main_sha)
except Exception as e:
if "already exists" not in str(e).lower():
raise
# Create branch in pool/pkgB
gitea_env.ensure_branch_exists("pool", "pkgB", "main")
pool_pkgb_main_sha = gitea_env._request("GET", "repos/pool/pkgB/branches/main").json()["commit"]["id"]
try:
gitea_env.create_branch("pool", "pkgB", branch_name, pool_pkgb_main_sha)
except Exception as e:
if "already exists" not in str(e).lower():
raise
custom_workflow_config = f"""{{
"Workflows": ["pr"],
"GitProjectName": "{test_full_repo_name}#{branch_name}",
"Organization": "pool",
"Branch": "{branch_name}",
"Reviewers": [ "-autogits_obs_staging_bot" ],
"ReviewRequired": true
}}"""
gitea_env.create_file(test_org_name, test_repo_name, "workflow.config", custom_workflow_config, branch=branch_name)
maintainership_content = """{
"": ["ownerX","ownerY"],
"pkgA": ["ownerA"],
"pkgB": ["ownerB","ownerBB"]
}"""
gitea_env.create_file(test_org_name, test_repo_name, "_maintainership.json", maintainership_content, branch=branch_name)
setup_users_from_config(gitea_env, custom_workflow_config, maintainership_content)
gitea_env.add_collaborator(test_org_name, test_repo_name, "autogits_obs_staging_bot", "write")
# Restart workflow-pr service
gitea_env.restart_service("workflow-pr")
time.sleep(1)
return gitea_env, test_full_repo_name, branch_name
@pytest.fixture(scope="session")
def ownerA_client(gitea_env: GiteaAPIClient):
"""
Returns a GiteaAPIClient instance for ownerA.
"""
admin_token = gitea_env.headers["Authorization"].split(" ")[1]
return GiteaAPIClient(base_url=gitea_env.base_url, token=admin_token, sudo="ownerA")
@pytest.fixture(scope="session")
def ownerB_client(gitea_env: GiteaAPIClient):
"""
Returns a GiteaAPIClient instance for ownerB.
"""
admin_token = gitea_env.headers["Authorization"].split(" ")[1]
return GiteaAPIClient(base_url=gitea_env.base_url, token=admin_token, sudo="ownerB")
@pytest.fixture(scope="session")
def ownerBB_client(gitea_env: GiteaAPIClient):
"""
Returns a GiteaAPIClient instance for ownerBB.
"""
admin_token = gitea_env.headers["Authorization"].split(" ")[1]
return GiteaAPIClient(base_url=gitea_env.base_url, token=admin_token, sudo="ownerBB")

View File

@@ -226,21 +226,6 @@ index 0000000..{pkg_b_sha}
self._request("PATCH", f"repos/{org_name}/{repo_name}", json=repo_data)
print(f"Repository settings for '{org_name}/{repo_name}' updated.")
def create_label(self, owner: str, repo: str, name: str, color: str = "#abcdef"):
print(f"--- Creating label '{name}' in {owner}/{repo} ---")
url = f"repos/{owner}/{repo}/labels"
data = {
"name": name,
"color": color
}
try:
self._request("POST", url, json=data)
print(f"Label '{name}' created.")
except requests.exceptions.HTTPError as e:
if e.response.status_code == 422: # Already exists
print(f"Label '{name}' already exists.")
else:
raise
def create_file(self, owner: str, repo: str, file_path: str, content: str, branch: str = "main", message: str = "Add file"):
file_info = self.get_file_info(owner, repo, file_path, branch=branch)

View File

@@ -1,97 +0,0 @@
import pytest
import re
import time
from pathlib import Path
from tests.lib.common_test_utils import (
GiteaAPIClient,
)
# =============================================================================
# TEST CASES
# =============================================================================
@pytest.mark.t001
@pytest.mark.xfail(reason="review pending label is not applied")
def test_001_project_pr_labels(label_env, staging_bot_client):
"""
Test scenario:
1. Setup custom workflow.config with Labels: { "StagingAuto": "staging/Backlog", "ReviewPending": "review/Pending" }.
2. Create a package PR in 'label-test' branch.
3. Make sure the workflow-pr service created related project PR in 'label-test' branch.
4. Wait for the project PR to have the label "staging/Backlog".
5. Post approval from autogits_obs_staging_bot.
6. Check that the project PR gets the label "review/Pending".
"""
gitea_env, test_full_repo_name, branch_name = label_env
# 1. Create a package PR
diff = """diff --git a/label_test_fixture.txt b/label_test_fixture.txt
new file mode 100644
index 0000000..e69de29
"""
print(f"--- Creating package PR in pool/pkgA on branch {branch_name} ---")
package_pr = gitea_env.create_gitea_pr("pool/pkgA", diff, "Test Labels Fixture", False, base_branch=branch_name)
package_pr_number = package_pr["number"]
print(f"Created package PR pool/pkgA#{package_pr_number}")
# 2. Make sure the workflow-pr service created related project PR
project_pr_number = None
print(f"Polling pool/pkgA PR #{package_pr_number} timeline for forwarded PR event...")
for _ in range(40):
time.sleep(1)
timeline_events = gitea_env.get_timeline_events("pool/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"products/SLFO/pulls/(\d+)", url_to_check)
if match:
project_pr_number = int(match.group(1))
break
if project_pr_number:
break
assert project_pr_number is not None, "Workflow bot did not create a project PR."
print(f"Found project PR: products/SLFO#{project_pr_number}")
# 3. Wait for the project PR to have the label "staging/Backlog"
print(f"Checking for 'staging/Backlog' label on project PR products/SLFO#{project_pr_number}...")
backlog_label_found = False
expected_backlog_label = "staging/Backlog"
for _ in range(20):
project_pr_details = gitea_env.get_pr_details("products/SLFO", project_pr_number)
labels = project_pr_details.get("labels", [])
label_names = [l["name"] for l in labels]
if expected_backlog_label in label_names:
backlog_label_found = True
break
time.sleep(1)
assert backlog_label_found, f"Project PR products/SLFO#{project_pr_number} does not have the expected label '{expected_backlog_label}'."
print(f"Project PR products/SLFO#{project_pr_number} has the expected label '{expected_backlog_label}'.")
# 4. Post approval from autogits_obs_staging_bot
print(f"--- Posting approval from autogits_obs_staging_bot on project PR products/SLFO#{project_pr_number} ---")
staging_bot_client.create_review("products/SLFO", project_pr_number, event="APPROVED", body="Staging OK")
# 5. Check that the project PR has the label "review/Pending"
print(f"Checking for 'review/Pending' label on project PR products/SLFO#{project_pr_number}...")
pending_label_found = False
expected_pending_label = "review/Pending"
for _ in range(20):
project_pr_details = gitea_env.get_pr_details("products/SLFO", project_pr_number)
labels = project_pr_details.get("labels", [])
label_names = [l["name"] for l in labels]
print(f"Current labels: {label_names}")
if expected_pending_label in label_names:
pending_label_found = True
break
time.sleep(1)
assert pending_label_found, f"Project PR products/SLFO#{project_pr_number} does not have the expected label '{expected_pending_label}'."
print(f"Project PR products/SLFO#{project_pr_number} has the expected label '{expected_pending_label}'.")

View File

@@ -5,43 +5,6 @@ import base64
from pathlib import Path
from tests.lib.common_test_utils import GiteaAPIClient
@pytest.mark.t001
def test_001_review_requests_matching_config(automerge_env, ownerA_client):
"""
Test scenario:
1. The package PR for pkgB is opened by ownerA (who is not a maintainer of pkgB).
2. Check that review request comes to ownerB and ownerBB (package maintainers)
AND usera and userb (from workflow.config).
"""
gitea_env, test_full_repo_name, branch_name = automerge_env
# 1. Create a package PR for pool/pkgB as ownerA
diff = """diff --git a/pkgB_test_001.txt b/pkgB_test_001.txt
new file mode 100644
index 0000000..e69de29
"""
print(f"--- Creating package PR in pool/pkgB on branch {branch_name} as ownerA ---")
package_pr = ownerA_client.create_gitea_pr("pool/pkgB", diff, "Test Review Requests Config", True, base_branch=branch_name)
package_pr_number = package_pr["number"]
print(f"Created package PR pool/pkgB#{package_pr_number}")
# 2. Check that review requests came to ownerB, ownerBB, usera, and userb
print("Checking for review requests from maintainers and workflow.config...")
reviewers_requested = set()
expected_reviewers = {"ownerB", "ownerBB", "usera", "userb"}
for _ in range(30):
reviews = gitea_env.list_reviews("pool/pkgB", package_pr_number)
reviewers_requested = {r["user"]["login"] for r in reviews if r["state"] == "REQUEST_REVIEW"}
if expected_reviewers.issubset(reviewers_requested):
break
time.sleep(1)
for reviewer in expected_reviewers:
assert reviewer in reviewers_requested, f"{reviewer} was not requested for review. Requested: {reviewers_requested}"
print(f"Confirmed: {expected_reviewers} were requested for review.")
@pytest.mark.t004
@pytest.mark.xfail(reason="the bot sometimes re-requests review from autogits_obs_staging_bot despite having the approval")
def test_004_maintainer(maintainer_env, ownerA_client):
@@ -248,7 +211,6 @@ index 0000000..e69de29
@pytest.mark.t006
@pytest.mark.xfail(reason="tbd flacky in ci")
def test_006_maintainer_rejection_removes_other_requests(maintainer_env, ownerA_client, ownerBB_client):
"""
Test scenario:

View File

@@ -6,7 +6,7 @@ COPY integration/rabbitmq-config/certs/cert.pem /usr/share/pki/trust/anchors/git
RUN update-ca-certificates
# Install git and ssh
RUN zypper -n in git-core openssh-clients binutils git-lfs || (tail -n 1000 /var/log/zypper.log; exit 1)
RUN zypper -n in git-core openssh-clients binutils git-lfs
# Copy the pre-built binary into the container
COPY workflow-pr/workflow-pr /usr/local/bin/workflow-pr

View File

@@ -9,7 +9,7 @@ RUN zypper ar -f http://download.opensuse.org/repositories/devel:/Factory:/git-w
RUN zypper --gpg-auto-import-keys ref
# Install git and ssh
RUN zypper -n in git-core openssh-clients autogits-workflow-pr binutils git-lfs || ( tail -n 1000 /var/log/zypper.log; exit 1 )
RUN zypper -n in git-core openssh-clients autogits-workflow-pr binutils git-lfs
COPY integration/workflow-pr/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +4755 /usr/local/bin/entrypoint.sh

View File

@@ -3,6 +3,5 @@
"products/SLFO#dev",
"products/SLFO#merge",
"products/SLFO#maintainer-merge",
"products/SLFO#review-required",
"products/SLFO#label-test"
"products/SLFO#review-required"
]

View File

@@ -50,6 +50,10 @@ const (
var runId uint
var GitWorkTreeAllocate func(string, string, string) (common.GitHandlerGenerator, error) = func(basePath, gitAuthor, email string) (common.GitHandlerGenerator, error) {
return common.AllocateGitWorkTree(basePath, gitAuthor, email)
}
func FetchPrGit(git common.Git, pr *models.PullRequest) error {
// clone PR head via base (target) repo
cloneURL := pr.Base.Repo.CloneURL
@@ -144,9 +148,9 @@ func ProcessBuildStatus(project *common.BuildResultList) BuildStatusSummary {
func ProcessRepoBuildStatus(results []*common.PackageBuildStatus) (status BuildStatusSummary) {
PackageBuildStatusSorter := func(a, b *common.PackageBuildStatus) int {
return strings.Compare(a.Package, b.Package)
}
PackageBuildStatusSorter := func(a, b *common.PackageBuildStatus) int {
return strings.Compare(a.Package, b.Package)
}
common.LogDebug("******* RESULTS: ")
data, _ := xml.MarshalIndent(results, "", " ")
@@ -191,24 +195,23 @@ func GetPackageBuildStatus(project *common.BuildResultList, packageName string)
return true, BuildStatusSummaryUnknown // true for 'missing'
}
// Check for any failures
// Check for any unfinished builds
for _, pkgStatus := range packageStatuses {
res, ok := common.ObsBuildStatusDetails[pkgStatus.Code]
if !ok {
common.LogInfo("unknown package result code:", pkgStatus.Code, "for package:", pkgStatus.Package)
return false, BuildStatusSummaryUnknown
}
if !res.Success {
return false, BuildStatusSummaryFailed
if !res.Finished {
return false, BuildStatusSummaryBuilding
}
}
// Check for any unfinished builds
// Check for any failures
for _, pkgStatus := range packageStatuses {
res, _ := common.ObsBuildStatusDetails[pkgStatus.Code]
// 'ok' is already checked in the loop above
if !res.Finished {
return false, BuildStatusSummaryBuilding
if !res.Success {
return false, BuildStatusSummaryFailed
}
}
@@ -216,7 +219,7 @@ func GetPackageBuildStatus(project *common.BuildResultList, packageName string)
return false, BuildStatusSummarySuccess
}
func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingPrj, buildPrj string, stagingMasterPrj string) (*common.ProjectMeta, error) {
func GenerateObsPrjMeta(obs common.ObsClientInterface, git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingPrj, buildPrj string, stagingMasterPrj string) (*common.ProjectMeta, error) {
common.LogDebug("repo content fetching ...")
err := FetchPrGit(git, pr)
if err != nil {
@@ -260,13 +263,13 @@ func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullReque
}
common.LogDebug("Trying first staging master project: ", stagingMasterPrj)
meta, err := ObsClient.GetProjectMeta(stagingMasterPrj)
meta, err := obs.GetProjectMeta(stagingMasterPrj)
if err == nil {
// success, so we use that staging master project as our build project
buildPrj = stagingMasterPrj
} else {
common.LogInfo("error fetching project meta for ", stagingMasterPrj, ". Fall Back to ", buildPrj)
meta, err = ObsClient.GetProjectMeta(buildPrj)
meta, err = obs.GetProjectMeta(buildPrj)
}
if err != nil {
common.LogError("error fetching project meta for", buildPrj, ". Err:", err)
@@ -330,10 +333,10 @@ func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullReque
// stagingProject:$buildProject
// ^- stagingProject:$buildProject:$subProjectName (based on templateProject)
func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingProject, templateProject, subProjectName string, buildDisableRepos []string) error {
func CreateQASubProject(obs common.ObsClientInterface, stagingConfig *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingProject, templateProject, subProjectName string, buildDisableRepos []string) error {
common.LogDebug("Setup QA sub projects")
common.LogDebug("reading templateProject ", templateProject)
templateMeta, err := ObsClient.GetProjectMeta(templateProject)
templateMeta, err := obs.GetProjectMeta(templateProject)
if err != nil {
common.LogError("error fetching template project meta for", templateProject, ":", err)
return err
@@ -343,10 +346,10 @@ func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, git
templateMeta.Name = stagingProject + ":" + subProjectName
// freeze tag for now
if len(templateMeta.ScmSync) > 0 {
repository, err := url.Parse(templateMeta.ScmSync)
if err != nil {
panic(err)
}
repository, err := url.Parse(templateMeta.ScmSync)
if err != nil {
panic(err)
}
common.LogDebug("getting data for ", repository.EscapedPath())
split := strings.Split(repository.EscapedPath(), "/")
@@ -354,12 +357,12 @@ func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, git
common.LogDebug("getting commit for ", org, " repo ", repo, " fragment ", repository.Fragment)
branch, err := gitea.GetCommit(org, repo, repository.Fragment)
if err != nil {
panic(err)
}
if err != nil {
panic(err)
}
// set expanded commit url
repository.Fragment = branch.SHA
repository.Fragment = branch.SHA
templateMeta.ScmSync = repository.String()
common.LogDebug("Setting scmsync url to ", templateMeta.ScmSync)
}
@@ -406,11 +409,11 @@ func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, git
templateMeta.Repositories[idx].Paths[pidx].Project = templateMeta.Name
} else
// Check for path prefixes against a template project inside of template project area
if strings.HasPrefix(path.Project, stagingConfig.StagingProject + ":") {
if strings.HasPrefix(path.Project, stagingConfig.StagingProject+":") {
newProjectName := stagingProject
// find project name
for _, setup := range stagingConfig.QA {
if setup.Origin == path.Project {
if setup.Origin == path.Project {
common.LogDebug(" Match:", setup.Origin)
newProjectName = newProjectName + ":" + setup.Name
common.LogDebug(" New:", newProjectName)
@@ -418,14 +421,14 @@ func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, git
}
}
templateMeta.Repositories[idx].Paths[pidx].Project = newProjectName
common.LogDebug(" Matched prefix")
common.LogDebug(" Matched prefix")
}
common.LogDebug(" Path using project ", templateMeta.Repositories[idx].Paths[pidx].Project)
}
}
if !IsDryRun {
err = ObsClient.SetProjectMeta(templateMeta)
err = obs.SetProjectMeta(templateMeta)
if err != nil {
common.LogError("cannot create project:", templateMeta.Name, err)
x, _ := xml.MarshalIndent(templateMeta, "", " ")
@@ -439,10 +442,10 @@ func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, git
return nil
}
func StartOrUpdateBuild(config *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest) (RequestModification, error) {
func StartOrUpdateBuild(obs common.ObsClientInterface, config *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest) (RequestModification, error) {
common.LogDebug("fetching OBS project Meta")
obsPrProject := GetObsProjectAssociatedWithPr(config, ObsClient.HomeProject, pr)
meta, err := ObsClient.GetProjectMeta(obsPrProject)
obsPrProject := GetObsProjectAssociatedWithPr(config, obs.GetHomeProject(), pr)
meta, err := obs.GetProjectMeta(obsPrProject)
if err != nil {
common.LogError("error fetching project meta for", obsPrProject, ":", err)
return RequestModificationNoChange, err
@@ -467,7 +470,7 @@ func StartOrUpdateBuild(config *common.StagingConfig, git common.Git, gitea comm
if meta == nil {
// new build
common.LogDebug(" Staging master:", config.StagingProject)
meta, err = GenerateObsPrjMeta(git, gitea, pr, obsPrProject, config.ObsProject, config.StagingProject)
meta, err = GenerateObsPrjMeta(obs, git, gitea, pr, obsPrProject, config.ObsProject, config.StagingProject)
if err != nil {
return RequestModificationNoChange, err
}
@@ -479,7 +482,7 @@ func StartOrUpdateBuild(config *common.StagingConfig, git common.Git, gitea comm
common.LogDebug("Creating build project:")
common.LogDebug(" meta:", string(x))
} else {
err = ObsClient.SetProjectMeta(meta)
err = obs.SetProjectMeta(meta)
if err != nil {
x, _ := xml.MarshalIndent(meta, "", " ")
common.LogDebug(" meta:", string(x))
@@ -550,7 +553,7 @@ func ParseNotificationToPR(thread *models.NotificationThread) (org string, repo
return
}
func ProcessPullNotification(gitea common.Gitea, thread *models.NotificationThread) {
func ProcessPullNotification(obs common.ObsClientInterface, gitea common.Gitea, thread *models.NotificationThread) {
defer func() {
err := recover()
if err != nil {
@@ -566,7 +569,7 @@ func ProcessPullNotification(gitea common.Gitea, thread *models.NotificationThre
}
common.LogInfo("processing PR:", org, "/", repo, "#", num)
done, err := ProcessPullRequest(gitea, org, repo, num)
done, err := ProcessPullRequest(obs, gitea, org, repo, num)
if !IsDryRun && err == nil && done {
gitea.SetNotificationRead(thread.ID)
} else if err != nil {
@@ -576,7 +579,7 @@ func ProcessPullNotification(gitea common.Gitea, thread *models.NotificationThre
var CleanedUpIssues []int64 = []int64{}
func CleanupPullNotification(gitea common.Gitea, thread *models.NotificationThread) (CleanupComplete bool) {
func CleanupPullNotification(obs common.ObsClientInterface, gitea common.Gitea, thread *models.NotificationThread) (CleanupComplete bool) {
defer func() {
err := recover()
if err != nil {
@@ -643,8 +646,8 @@ func CleanupPullNotification(gitea common.Gitea, thread *models.NotificationThre
return false
}
stagingProject := GetObsProjectAssociatedWithPr(config, ObsClient.HomeProject, pr)
if prj, err := ObsClient.GetProjectMeta(stagingProject); err != nil {
stagingProject := GetObsProjectAssociatedWithPr(config, obs.GetHomeProject(), pr)
if prj, err := obs.GetProjectMeta(stagingProject); err != nil {
common.LogError("Failed fetching meta for project:", stagingProject, ". Not cleaning up")
return false
} else if prj == nil && err == nil {
@@ -658,13 +661,13 @@ func CleanupPullNotification(gitea common.Gitea, thread *models.NotificationThre
project := stagingProject + ":" + qa.Name
common.LogDebug("Cleaning up QA staging", project)
if !IsDryRun {
if err := ObsClient.DeleteProject(project); err != nil {
if err := obs.DeleteProject(project); err != nil {
common.LogError("Failed to cleanup QA staging", project, err)
}
}
}
if !IsDryRun {
if err := ObsClient.DeleteProject(stagingProject); err != nil {
if err := obs.DeleteProject(stagingProject); err != nil {
common.LogError("Failed to cleanup staging", stagingProject, err)
}
}
@@ -685,7 +688,7 @@ func SetStatus(gitea common.Gitea, org, repo, hash string, status *models.Commit
return err
}
func commentOnPackagePR(gitea common.Gitea, org string, repo string, prNum int64, msg string) {
func CommentPROnce(gitea common.Gitea, org string, repo string, prNum int64, msg string) {
if IsDryRun {
common.LogInfo("Would comment on package PR %s/%s#%d: %s", org, repo, prNum, msg)
return
@@ -697,6 +700,18 @@ func commentOnPackagePR(gitea common.Gitea, org string, repo string, prNum int64
return
}
timeline, err := gitea.GetTimeline(org, repo, prNum)
if err != nil {
common.LogError("Failed to get timeline for PR %s/%s#%d: %v", org, repo, prNum, err)
return
}
for _, t := range timeline {
if t.User != nil && t.User.UserName == BotUser && t.Type == common.TimelineCommentType_Comment && t.Body == msg {
return
}
}
err = gitea.AddComment(pr, msg)
if err != nil {
common.LogError("Failed to comment on package PR %s/%s#%d: %v", org, repo, prNum, err)
@@ -704,20 +719,21 @@ func commentOnPackagePR(gitea common.Gitea, org string, repo string, prNum int64
}
// Create and remove QA projects
func ProcessQaProjects(stagingConfig *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingProject string) []string {
func ProcessQaProjects(obs common.ObsClientInterface, stagingConfig *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingProject string) ([]string, string) {
usedQAprojects := make([]string, 0)
prLabelNames := make(map[string]int)
for _, label := range pr.Labels {
prLabelNames[label.Name] = 1
}
msg := ""
var qa_projects []string
for _, setup := range stagingConfig.QA {
QAproject := stagingProject + ":" + setup.Name
if len(setup.Label) > 0 {
if _, ok := prLabelNames[setup.Label]; !ok {
if !IsDryRun {
// blindly remove, will fail when not existing
ObsClient.DeleteProject(QAproject)
obs.DeleteProject(QAproject)
}
common.LogInfo("QA project ", setup.Name, "has no matching Label")
continue
@@ -726,24 +742,25 @@ func ProcessQaProjects(stagingConfig *common.StagingConfig, git common.Git, gite
usedQAprojects = append(usedQAprojects, QAproject)
// check for existens first, no error, but no meta is a 404
if meta, err := ObsClient.GetProjectMeta(QAproject); meta == nil && err == nil {
if meta, err := obs.GetProjectMeta(QAproject); meta == nil && err == nil {
common.LogInfo("Create QA project ", QAproject)
CreateQASubProject(stagingConfig, git, gitea, pr,
CreateQASubProject(obs, stagingConfig, git, gitea, pr,
stagingProject,
setup.Origin,
setup.Name,
setup.BuildDisableRepos)
msg = msg + "QA Project added: " + ObsWebHost + "/project/show/" +
QAproject + "\n"
qa_projects = append(qa_projects, ObsWebHost+"/project/show/"+QAproject)
}
}
if len(msg) > 1 {
gitea.AddComment(pr, msg)
if len(qa_projects) > 0 {
msg = "Additional QA builds:\n" + strings.Join(qa_projects, "\n")
}
return usedQAprojects
return usedQAprojects, msg
}
func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, error) {
func ProcessPullRequest(obs common.ObsClientInterface, gitea common.Gitea, org, repo string, id int64) (bool, error) {
dir, err := os.MkdirTemp(os.TempDir(), BotName)
common.PanicOnError(err)
if IsDryRun {
@@ -752,7 +769,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
defer os.RemoveAll(dir)
}
gh, err := common.AllocateGitWorkTree(dir, GitAuthor, "noaddress@suse.de")
gh, err := GitWorkTreeAllocate(dir, GitAuthor, "noaddress@suse.de")
common.PanicOnError(err)
git, err := gh.CreateGitHandler(org)
@@ -797,7 +814,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
if err != nil {
common.LogError("Staging config", common.StagingConfigFile, "not found in PR to the project. Aborting.")
if !IsDryRun {
_, err = gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Cannot find project config in PR: "+common.ProjectConfigFile)
_, _ = gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Cannot find project config in PR: "+common.ProjectConfigFile)
}
return true, err
}
@@ -817,7 +834,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
return true, nil
}
meta, err := ObsClient.GetProjectMeta(stagingConfig.ObsProject)
meta, err := obs.GetProjectMeta(stagingConfig.ObsProject)
if err != nil || meta == nil {
common.LogError("Cannot find reference project meta:", stagingConfig.ObsProject, err)
if !IsDryRun && err == nil {
@@ -946,8 +963,8 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
}
common.LogDebug("ObsProject:", stagingConfig.ObsProject)
stagingProject := GetObsProjectAssociatedWithPr(stagingConfig, ObsClient.HomeProject, pr)
change, err := StartOrUpdateBuild(stagingConfig, git, gitea, pr)
stagingProject := GetObsProjectAssociatedWithPr(stagingConfig, obs.GetHomeProject(), pr)
change, err := StartOrUpdateBuild(obs, stagingConfig, git, gitea, pr)
status := &models.CommitStatus{
Context: BotName,
Description: "OBS Staging build",
@@ -978,11 +995,8 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
SetStatus(gitea, org, repo, pr.Head.Sha, status)
}
if change != RequestModificationNoChange && !IsDryRun {
gitea.AddComment(pr, msg)
}
stagingResult, err := ObsClient.BuildStatus(stagingProject)
stagingResult, err := obs.BuildStatus(stagingProject)
if err != nil {
common.LogError("failed fetching stage project status for", stagingProject, ":", err)
}
@@ -990,7 +1004,14 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
_, packagePRs := common.ExtractDescriptionAndPRs(bufio.NewScanner(strings.NewReader(pr.Body)))
// always update QA projects because Labels can change
qaProjects := ProcessQaProjects(stagingConfig, git, gitea, pr, stagingProject)
qaProjects, qaProjectMsg := ProcessQaProjects(obs, stagingConfig, git, gitea, pr, stagingProject)
if change != RequestModificationNoChange && !IsDryRun {
if len(qaProjectMsg) > 0 {
msg += "\n" + qaProjectMsg
}
CommentPROnce(gitea, org, repo, id, msg)
}
done := false
overallBuildStatus := ProcessBuildStatus(stagingResult)
@@ -998,7 +1019,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
if len(qaProjects) > 0 && overallBuildStatus == BuildStatusSummarySuccess {
seperator := " in "
for _, qaProject := range qaProjects {
qaResult, err := ObsClient.BuildStatus(qaProject)
qaResult, err := obs.BuildStatus(qaProject)
if err != nil {
common.LogError("failed fetching stage project status for", qaProject, ":", err)
}
@@ -1058,7 +1079,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
default:
continue
}
commentOnPackagePR(gitea, packagePR.Org, packagePR.Repo, packagePR.Num, msg)
CommentPROnce(gitea, packagePR.Org, packagePR.Repo, packagePR.Num, msg)
}
if len(missingPkgs) > 0 {
@@ -1068,10 +1089,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
msg = msg + " - " + pkg + "\n"
}
common.LogInfo(msg)
err := gitea.AddComment(pr, msg)
if err != nil {
common.LogError(err)
}
CommentPROnce(gitea, org, repo, id, msg)
}
}
@@ -1090,8 +1108,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
return false, nil
}
func PollWorkNotifications(giteaUrl string) {
gitea := common.AllocateGiteaTransport(giteaUrl)
func PollWorkNotifications(obs common.ObsClientInterface, gitea common.Gitea) {
data, err := gitea.GetNotifications(common.GiteaNotificationType_Pull, nil)
if err != nil {
@@ -1107,7 +1124,7 @@ func PollWorkNotifications(giteaUrl string) {
if !ListPullNotificationsOnly {
switch notification.Subject.Type {
case "Pull":
ProcessPullNotification(gitea, notification)
ProcessPullNotification(obs, gitea, notification)
default:
if !IsDryRun {
gitea.SetNotificationRead(notification.ID)
@@ -1130,7 +1147,7 @@ func PollWorkNotifications(giteaUrl string) {
continue
}
cleanupFinished = CleanupPullNotification(gitea, n) && cleanupFinished
cleanupFinished = CleanupPullNotification(obs, gitea, n) && cleanupFinished
}
} else if err != nil {
common.LogError(err)
@@ -1144,7 +1161,8 @@ var ObsApiHost string
var ObsWebHost string
var IsDryRun bool
var ProcessPROnly string
var ObsClient *common.ObsClient
var ObsClient common.ObsClientInterface
var BotUser string
func ObsWebHostFromApiHost(apihost string) string {
u, err := url.Parse(apihost)
@@ -1209,9 +1227,18 @@ func main() {
}
if len(*buildRoot) > 0 {
ObsClient.HomeProject = *buildRoot
ObsClient.SetHomeProject(*buildRoot)
}
gitea := common.AllocateGiteaTransport(GiteaUrl)
user, err := gitea.GetCurrentUser()
if err != nil {
common.LogError("Cannot fetch current user:", err)
return
}
BotUser = user.UserName
if len(*ProcessPROnly) > 0 {
rx := regexp.MustCompile("^([^/#]+)/([^/#]+)#([0-9]+)$")
m := rx.FindStringSubmatch(*ProcessPROnly)
@@ -1220,15 +1247,14 @@ func main() {
return
}
gitea := common.AllocateGiteaTransport(GiteaUrl)
id, _ := strconv.ParseInt(m[3], 10, 64)
ProcessPullRequest(gitea, m[1], m[2], id)
ProcessPullRequest(ObsClient, gitea, m[1], m[2], id)
return
}
for {
PollWorkNotifications(GiteaUrl)
PollWorkNotifications(ObsClient, gitea)
common.LogInfo("Poll cycle finished")
time.Sleep(5 * time.Minute)
}

File diff suppressed because it is too large Load Diff