15 Commits

Author SHA256 Message Date
ebc34f9645 Add an "lfs fsck" check after submodule update
Some checks failed
go-generate-check / go-generate-check (pull_request) Failing after 23s
to avoid merging pull requests where lfs objects are not correctly
registered. Can happen when user has not installed lfs for example.
2026-01-13 14:00:28 +01:00
9b5ea9166f Transfer WIP: from package pool requests to project 2025-12-10 15:39:02 +01:00
3e25d3070b Disable temporary comment adding in case of lacking permissions 2025-12-10 15:39:02 +01:00
0a26753ec9 pr: only update PR if elided title not changed
Gitea trims long titles so we need to compare if the trimmed length
is same, not entire string that will always differ.
2025-12-10 15:39:02 +01:00
38bf74680d Support remote source in pull requests
This requires write permission by maintainer there
2025-12-10 15:39:02 +01:00
877df11ebe workflow-direct: use relative path when adding a submodule
This solves the issue of using the right credentials based on the
main repo.
Also it allows to rename the organisation.
2025-12-10 15:39:01 +01:00
16dfa6bd15 HACK: disable close of pullrequests for now
it hit too many ones
2025-12-10 15:39:01 +01:00
2373723b1a workflow-pr: don't crash when no submodule got changed
eg. config changes only
2025-12-10 15:39:01 +01:00
a68fcaf8d9 obs-staging-bot: Get rid of reference project results entirely
Currently all our release managers insist anyway of only green states
2025-12-10 15:39:01 +01:00
6630fbd843 obs-staging-bot: fix wrong acceptance step 2
remove more code to ignore reference project entirely
2025-12-10 15:39:01 +01:00
8b5ddfde47 Fix build status check when package source is in subdir
We must not use the upper directory name as part of the package name.
2025-12-10 15:39:01 +01:00
1e902dcb29 obs-staging-bot: fix wrong acceptance
Any pull request with multiple packages was always successful, just when
one package  was successful build.

Dropping the "someSuccess" handling for now since it is currently
broken and I got also complaints that we anyway never should accept
broken submissions. Even when the failure did exist before.

This would anyway break an mainteannce/patchinfo build.

We may want to add a config option to allow the again under given
circumstances.
2025-12-10 15:39:01 +01:00
b6b70f17cb obs-staging: fix onlybuild for subdirectories
The parameter only takes a package name, but not a full subdirectory
string. So we hand over only the last part behind a / here
2025-12-10 15:39:01 +01:00
7ff261fcf5 obs_staging: fix results compare
Esp. with multiple repositories:
* j counter was not reset
* wrong limit of results used (from reference not stage project)
* package name compare using == was always wrong (and unneeded here)
2025-12-10 15:39:01 +01:00
92dbaf4c9a Staging: include also changed directories
Projects which are not using submodules as package directories
still need to be handled in stagings.
2025-12-10 15:39:01 +01:00
27 changed files with 288 additions and 1644 deletions

View File

@@ -169,7 +169,6 @@ install -D -m0755 gitea-events-rabbitmq-publisher/gitea-events-rabbitmq-publishe
install -D -m0644 systemd/gitea-events-rabbitmq-publisher.service %{buildroot}%{_unitdir}/gitea-events-rabbitmq-publisher.service
install -D -m0755 gitea_status_proxy/gitea_status_proxy %{buildroot}%{_bindir}/gitea_status_proxy
install -D -m0755 group-review/group-review %{buildroot}%{_bindir}/group-review
install -D -m0644 systemd/group-review@.service %{buildroot}%{_unitdir}/group-review@.service
install -D -m0755 obs-forward-bot/obs-forward-bot %{buildroot}%{_bindir}/obs-forward-bot
install -D -m0755 obs-staging-bot/obs-staging-bot %{buildroot}%{_bindir}/obs-staging-bot
install -D -m0644 systemd/obs-staging-bot.service %{buildroot}%{_unitdir}/obs-staging-bot.service
@@ -192,18 +191,6 @@ install -D -m0755 utils/hujson/hujson
%postun gitea-events-rabbitmq-publisher
%service_del_postun gitea-events-rabbitmq-publisher.service
%pre group-review
%service_add_pre group-review@.service
%post group-review
%service_add_post group-review@.service
%preun group-review
%service_del_preun group-review@.service
%postun group-review
%service_del_postun group-review@.service
%pre obs-staging-bot
%service_add_pre obs-staging-bot.service
@@ -264,7 +251,6 @@ install -D -m0755 utils/hujson/hujson
%license COPYING
%doc group-review/README.md
%{_bindir}/group-review
%{_unitdir}/group-review@.service
%files obs-forward-bot
%license COPYING

View File

@@ -54,7 +54,6 @@ type ReviewGroup struct {
type QAConfig struct {
Name string
Origin string
BuildDisableRepos []string // which repos to build disable in the new project
}
type Permissions struct {
@@ -63,9 +62,9 @@ type Permissions struct {
}
const (
Label_StagingAuto = "staging/Auto"
Label_ReviewPending = "review/Pending"
Label_ReviewDone = "review/Done"
Label_StagingAuto = "staging/Auto"
Label_ReviewPending = "review/Pending"
Label_ReviewDone = "review/Done"
)
func LabelKey(tag_value string) string {

View File

@@ -283,7 +283,7 @@ func (e *GitHandlerImpl) GitClone(repo, branch, remoteUrl string) (string, error
func (e *GitHandlerImpl) GitBranchHead(gitDir, branchName string) (string, error) {
id, err := e.GitExecWithOutput(gitDir, "show-ref", "--heads", "--hash", branchName)
if err != nil {
return "", fmt.Errorf("Can't find default branch: %s", branchName)
return "", fmt.Errorf("Can't find default branch: %s in %s", branchName, gitDir)
}
id = strings.TrimSpace(SplitLines(id)[0])
@@ -297,7 +297,7 @@ func (e *GitHandlerImpl) GitBranchHead(gitDir, branchName string) (string, error
func (e *GitHandlerImpl) GitRemoteHead(gitDir, remote, branchName string) (string, error) {
id, err := e.GitExecWithOutput(gitDir, "show-ref", "--hash", "--verify", "refs/remotes/"+remote+"/"+branchName)
if err != nil {
return "", fmt.Errorf("Can't find default branch: %s", branchName)
return "", fmt.Errorf("Can't find default branch: %s in %s", branchName, gitDir)
}
return strings.TrimSpace(id), nil
@@ -904,6 +904,7 @@ func (e *GitHandlerImpl) GitSubmoduleList(gitPath, commitId string) (submoduleLi
for _, te := range tree.items {
if te.isTree() {
trees[p+te.name+"/"] = te.hash
submoduleList[p+te.name] = te.hash
} else if te.isSubmodule() {
submoduleList[p+te.name] = te.hash
}

View File

@@ -689,13 +689,15 @@ func (r *BuildResultList) BuildResultSummary() (success, finished bool) {
if !ok {
panic("Unknown result code: " + result.Code)
}
if r.isLastBuild && result.Code == "unknown" {
// it means the package has never build yet,
// but we don't know the reason
detail.Finished = true
if r.isLastBuild {
// we are always finished, since it is the last result
// also when there is "unknown" state, it just means it
// it was never done
finished = true
} else {
finished = finished && detail.Finished
}
finished = finished && detail.Finished
success = success && detail.Success
if !finished {

View File

@@ -23,8 +23,7 @@ type PRSet struct {
PRs []*PRInfo
Config *AutogitConfig
BotUser string
HasAutoStaging bool
BotUser string
}
func (prinfo *PRInfo) PRComponents() (org string, repo string, idx int64) {
@@ -124,19 +123,14 @@ func LastPrjGitRefOnTimeline(botUser string, gitea GiteaPRTimelineReviewFetcher,
}
pr, err := gitea.GetPullRequest(prjGitOrg, prjGitRepo, issue.Index)
if err != nil {
switch err.(type) {
case *repository.RepoGetPullRequestNotFound: // deleted?
continue
default:
LogDebug("PrjGit RefIssue fetch error from timeline", issue.Index, err)
continue
}
switch err.(type) {
case *repository.RepoGetPullRequestNotFound: // deleted?
continue
default:
LogDebug("PrjGit RefIssue fetch error from timeline", issue.Index, err)
}
LogDebug("found ref PR on timeline:", PRtoString(pr))
if pr.Base.Name != prjGitBranch {
LogDebug(" -> not matching:", pr.Base.Name, prjGitBranch)
if pr.Base.Ref != prjGitBranch {
continue
}
@@ -309,9 +303,6 @@ func (rs *PRSet) FindMissingAndExtraReviewers(maintainers MaintainershipData, id
pr := rs.PRs[idx]
if rs.IsPrjGitPR(pr.PR) {
missing = slices.Concat(configReviewers.Prj, configReviewers.PrjOptional)
if rs.HasAutoStaging {
missing = append(missing, Bot_BuildReview)
}
LogDebug("PrjGit submitter:", pr.PR.User.UserName)
// only need project maintainer reviews if:
// * not created by a bot and has other PRs, or
@@ -500,9 +491,6 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
var pkg string
if rs.IsPrjGitPR(pr.PR) {
reviewers = configReviewers.Prj
if rs.HasAutoStaging {
reviewers = append(reviewers, Bot_BuildReview)
}
pkg = ""
} else {
reviewers = configReviewers.Pkg

View File

@@ -631,8 +631,6 @@ func TestFindMissingAndExtraReviewers(t *testing.T) {
prset *common.PRSet
maintainers common.MaintainershipData
noAutoStaging bool
expected_missing_reviewers [][]string
expected_extra_reviewers [][]string
}{
@@ -690,40 +688,7 @@ func TestFindMissingAndExtraReviewers(t *testing.T) {
[]string{"autogits_obs_staging_bot", "user1"},
},
},
{
name: "One project reviewer only and no auto staging",
noAutoStaging: true,
prset: &common.PRSet{
PRs: []*common.PRInfo{
{
PR: &models.PullRequest{
User: &models.User{UserName: "foo"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
},
Reviews: &common.PRReviews{},
},
{
PR: &models.PullRequest{
User: &models.User{UserName: "foo"},
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
},
Reviews: &common.PRReviews{},
},
},
Config: &common.AutogitConfig{
GitProjectName: "prg/repo#main",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1"},
},
},
maintainers: &common.MaintainershipMap{Data: map[string][]string{}},
expected_missing_reviewers: [][]string{
nil,
{"user1"},
},
},
{
name: "One project reviewer and one pkg reviewer only",
prset: &common.PRSet{
@@ -1099,7 +1064,6 @@ func TestFindMissingAndExtraReviewers(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test.prset.HasAutoStaging = !test.noAutoStaging
for idx, pr := range test.prset.PRs {
missing, extra := test.prset.FindMissingAndExtraReviewers(test.maintainers, idx)

View File

@@ -1,5 +1,9 @@
package common
import (
"slices"
)
type Reviewers struct {
Prj []string
Pkg []string
@@ -32,5 +36,10 @@ func ParseReviewers(input []string) *Reviewers {
*pkg = append(*pkg, reviewer)
}
}
if !slices.Contains(r.Prj, Bot_BuildReview) {
r.Prj = append(r.Prj, Bot_BuildReview)
}
return r
}

View File

@@ -21,14 +21,14 @@ func TestReviewers(t *testing.T) {
name: "project and package reviewers",
input: []string{"1", "2", "3", "*5", "+6", "-7"},
prj: []string{"5", "7"},
prj: []string{"5", "7", common.Bot_BuildReview},
pkg: []string{"1", "2", "3", "5", "6"},
},
{
name: "optional project and package reviewers",
input: []string{"~1", "2", "3", "~*5", "+6", "-7"},
prj: []string{"7"},
prj: []string{"7", common.Bot_BuildReview},
pkg: []string{"2", "3", "6"},
prj_optional: []string{"5"},
pkg_optional: []string{"1", "5"},

View File

@@ -27,87 +27,10 @@ import (
"regexp"
"slices"
"strings"
"unicode"
"src.opensuse.org/autogits/common/gitea-generated/models"
)
type NewRepos struct {
Repos []struct {
Organization, Repository, Branch string
PackageName string
}
IsMaintainer bool
}
const maintainership_line = "MAINTAINER"
var true_lines []string = []string{"1", "TRUE", "YES", "OK", "T"}
func HasSpace(s string) bool {
return strings.IndexFunc(s, unicode.IsSpace) >= 0
}
func FindNewReposInIssueBody(body string) *NewRepos {
Issues := &NewRepos{}
for _, line := range strings.Split(body, "\n") {
line = strings.TrimSpace(line)
if ul := strings.ToUpper(line); strings.HasPrefix(ul, "MAINTAINER") {
value := ""
if idx := strings.IndexRune(ul, ':'); idx > 0 && len(ul) > idx+2 {
value = ul[idx+1:]
} else if idx := strings.IndexRune(ul, ' '); idx > 0 && len(ul) > idx+2 {
value = ul[idx+1:]
}
if slices.Contains(true_lines, strings.TrimSpace(value)) {
Issues.IsMaintainer = true
}
}
// line = strings.TrimSpace(line)
issue := struct{ Organization, Repository, Branch, PackageName string }{}
branch := strings.Split(line, "#")
repo := strings.Split(branch[0], "/")
if len(branch) == 2 {
issue.Branch = strings.TrimSpace(branch[1])
}
if len(repo) == 2 {
issue.Organization = strings.TrimSpace(repo[0])
issue.Repository = strings.TrimSpace(repo[1])
issue.PackageName = issue.Repository
if idx := strings.Index(strings.ToUpper(issue.Branch), " AS "); idx > 0 && len(issue.Branch) > idx+5 {
issue.PackageName = strings.TrimSpace(issue.Branch[idx+3:])
issue.Branch = strings.TrimSpace(issue.Branch[0:idx])
}
if HasSpace(issue.Organization) || HasSpace(issue.Repository) || HasSpace(issue.PackageName) || HasSpace(issue.Branch) {
continue
}
} else {
continue
}
Issues.Repos = append(Issues.Repos, issue)
//PackageNameIdx := strings.Index(strings.ToUpper(line), " AS ")
//words := strings.Split(line)
}
if len(Issues.Repos) == 0 {
return nil
}
return Issues
}
func IssueToString(issue *models.Issue) string {
if issue == nil {
return "(nil)"
}
return fmt.Sprintf("%s/%s#%d", issue.Repository.Owner, issue.Repository.Name, issue.Index)
}
func SplitLines(str string) []string {
return SplitStringNoEmpty(str, "\n")
}

View File

@@ -1,7 +1,6 @@
package common_test
import (
"reflect"
"testing"
"src.opensuse.org/autogits/common"
@@ -221,87 +220,3 @@ func TestRemovedBranchName(t *testing.T) {
})
}
}
func TestNewPackageIssueParsing(t *testing.T) {
tests := []struct {
name string
input string
issues *common.NewRepos
}{
{
name: "Nothing",
},
{
name: "Basic repo",
input: "org/repo#branch",
issues: &common.NewRepos{
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
{Organization: "org", Repository: "repo", Branch: "branch", PackageName: "repo"},
},
},
},
{
name: "Default branch and junk lines and approval for maintainership",
input: "\n\nsome comments\n\norg1/repo2\n\nmaintainership: yes",
issues: &common.NewRepos{
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
{Organization: "org1", Repository: "repo2", Branch: "", PackageName: "repo2"},
},
IsMaintainer: true,
},
},
{
name: "Default branch and junk lines and no maintainership",
input: "\n\nsome comments\n\norg1/repo2\n\nmaintainership: NEVER",
issues: &common.NewRepos{
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
{Organization: "org1", Repository: "repo2", Branch: "", PackageName: "repo2"},
},
},
},
{
name: "3 repos with comments and maintainership",
input: "\n\nsome comments for org1/repo2 are here and more\n\norg1/repo2#master\n org2/repo3#master\n some/repo3#m\nMaintainer ok",
issues: &common.NewRepos{
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
{Organization: "org1", Repository: "repo2", Branch: "master", PackageName: "repo2"},
{Organization: "org2", Repository: "repo3", Branch: "master", PackageName: "repo3"},
{Organization: "some", Repository: "repo3", Branch: "m", PackageName: "repo3"},
},
IsMaintainer: true,
},
},
{
name: "Invalid repos with spaces",
input: "or g/repo#branch\norg/r epo#branch\norg/repo#br anch\norg/repo#branch As foo ++",
},
{
name: "Valid repos with spaces",
input: " org / repo # branch",
issues: &common.NewRepos{
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
{Organization: "org", Repository: "repo", Branch: "branch", PackageName: "repo"},
},
},
},
{
name: "Package name is not repo name",
input: " org / repo # branch as repo++ \nmaintainer true",
issues: &common.NewRepos{
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
{Organization: "org", Repository: "repo", Branch: "branch", PackageName: "repo++"},
},
IsMaintainer: true,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
issue := common.FindNewReposInIssueBody(test.input)
if !reflect.DeepEqual(test.issues, issue) {
t.Error("Expected", test.issues, "but have", issue)
}
})
}
}

View File

@@ -1,187 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use IPC::Open2;
use URI;
sub FindFactoryCommit {
my ($package) = @_;
# Execute osc cat and capture output
my $osc_cmd = "osc cat openSUSE:Factory $package $package.changes";
open( my $osc_fh, "$osc_cmd |" ) or die "Failed to run osc: $!";
my $data = do { local $/; <$osc_fh> };
close($osc_fh);
# Calculate size
my $size = length($data);
# Create blob header
my $blob = "blob $size\0$data";
# Open a pipe to openssl to compute the hash
my ( $reader, $writer );
my $pid = open2( $reader, $writer, "openssl sha256" );
# Send blob data
print $writer $blob;
close $writer;
# Read the hash result and extract it
my $hash_line = <$reader>;
waitpid( $pid, 0 );
my ($hash) = $hash_line =~ /([a-fA-F0-9]{64})/;
# Run git search command with the hash
print("looking for hash: $hash\n");
my @hashes;
my $git_cmd =
"git -C $package rev-list --all pool/HEAD | while read commit; do git -C $package ls-tree \"\$commit\" | grep -q '^100644 blob $hash' && echo \"\$commit\"; done";
open( my $git_fh, "$git_cmd |" ) or die "Failed to run git search: $!";
while ( my $commit = <$git_fh> ) {
chomp $commit;
print "Found commit $commit\n";
push( @hashes, $commit );
}
close($git_fh);
return @hashes;
}
sub FactoryMd5 {
my ($package) = @_;
my $out = "";
if (system("osc ls openSUSE:Factory $package | grep -q build.specials.obscpio") == 0) {
system("mkdir _extract") == 0 || die "_extract exists or can't make it. Aborting.";
chdir("_extract") || die;
system("osc cat openSUSE:Factory $package build.specials.obscpio | cpio -dium 2> /dev/null") == 0 || die;
system("rm .* 2> /dev/null");
open( my $fh, "find -type f -exec /usr/bin/basename {} \\; | xargs md5sum | awk '{print \$1 FS \$2}' | grep -v d41d8cd98f00b204e9800998ecf8427e |") or die;
while ( my $l = <$fh>) {
$out = $out.$l;
}
close($fh);
chdir("..") && system("rm -rf _extract") == 0 || die;
}
open( my $fh, "osc ls -v openSUSE:Factory $package | awk '{print \$1 FS \$7}' | grep -v -F '_scmsync.obsinfo\nbuild.specials.obscpio' |") or die;
while (my $l = <$fh>) {
$out = $out.$l;
}
close($fh);
return $out;
}
# Read project from first argument
sub Usage {
die "Usage: $0 <OBS Project> <package> <repo>";
}
my $project = shift or Usage();
my $pkg = shift;
my $repo = shift;
if (not defined($repo)) {
Usage();
}
my $meta_url = `osc meta pkg $project $pkg | grep scmsync | sed -e 's,\\s*</\\?scmsync>\\s*,,g'`;
chomp($meta_url);
if ($meta_url ne $repo) {
die "meta not equal to repo for $pkg: $meta_url != $repo";
}
my $u = URI->new($meta_url);
die "Only src.opensuse.org is supported" unless $u->scheme =~ /^https?$/ && $u->host eq 'src.opensuse.org';
my (undef, $org, $repo_path) = split('/', $u->path);
my $branch = $u->fragment;
die "Only src.opensuse.org is supported" unless $org;
if ($org eq "pool") {
print "Already a pool package. We are done.\n";
exit(0);
}
my %params = $u->query_form;
delete $params{trackingbranch};
die "Unsupported query parameters: " . join(', ', keys %params) if keys %params;
my @packages = ($pkg) if defined $pkg;
if ( ! -e $org ) {
mkdir($org);
}
chdir($org);
my $super_user = $ENV{SUPER};
if (defined($super_user)) {
$super_user = "-G $super_user";
} else {
$super_user = "";
}
for my $package ( sort(@packages) ) {
print " ----- PROCESSING $package\n";
my $url = "https://src.opensuse.org/$org/$repo_path.git";
my $push_url = "gitea\@src.opensuse.org:pool/$package.git";
if ( not -e $package ) {
print("cloning...\n");
system("git clone --origin pool $url $package") == 0
or die "Can't clone $org/$repo_path";
}
else {
print("adding remote...\n");
system("git -C $package remote rm pool > /dev/null");
system("git -C $package remote add pool $url") == 0
or die "Can't add pool for $package";
}
system("git -C $package remote set-url pool --push $push_url") == 0
or die "Can't add push remote for $package";
print("fetching remote...\n");
system("git -C $package fetch pool") == 0 or die "Can't fetch pool for $package";
my @commits = FindFactoryCommit($package);
my $Md5Hashes = FactoryMd5($package);
my $c;
my $match = 0;
for my $commit (@commits) {
if ( length($commit) != 64 ) {
print("Failed to find factory commit. Aborting.");
exit(1);
}
if (
system("git -C $package lfs fetch pool $commit") == 0
and system("git -C $package checkout -B factory $commit") == 0
and system("git -C $package lfs checkout") == 0
and chdir($package)) {
open(my $fh, "|-", "md5sum -c --quiet") or die $!;
print $fh $Md5Hashes;
close $fh;
if ($? >> 8 != 0) {
chdir("..") || die;
next;
}
open($fh, "|-", "awk '{print \$2}' | sort | bash -c \"diff <(ls -1 | sort) -\"") or die $!;
print $fh $Md5Hashes;
close $fh;
my $ec = $? >> 8;
chdir("..") || die;
if ($ec == 0) {
$c = $commit;
$match = 1;
last;
}
}
}
if ( !$match ) {
die "Match not found. Aborting.";
}
system ("git -C $package push -f pool factory");
print "$package: $c\n";
}

View File

@@ -1,4 +1,3 @@
SystemsManagement
Java:packages
Kernel:firmware
Kernel:kdump

View File

@@ -1,4 +0,0 @@
#!/usr/bin/bash
osc api '/search/package?match=scmsync' | ../xml_package_parse | ../find_factory_commit_in_gitpkg.pl

View File

@@ -1,83 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use XML::Parser;
my $parser = XML::Parser->new(Handlers => {
Start => \&handle_start,
End => \&handle_end,
Char => \&handle_char,
});
my $current_element = '';
my $current_package_attrs = {};
my $scmsync_content = '';
my %devel_pkgs;
open(my $dfh, "curl -s https://src.opensuse.org/openSUSE/Factory/raw/branch/main/pkgs/_meta/devel_packages |") or die $!;
while(<$dfh>) {
chomp;
$devel_pkgs{$_} = 1;
}
close($dfh);
my $xml_content = do { local $/; <STDIN> };
$parser->parse($xml_content);
sub handle_start {
my ($expat, $element, %attrs) = @_;
$current_element = $element;
if ($element eq 'package') {
$current_package_attrs = \%attrs;
}
if ($element eq 'scmsync') {
$scmsync_content = '';
}
}
sub handle_char {
my ($expat, $string) = @_;
if ($current_element eq 'scmsync') {
$scmsync_content .= $string;
}
}
sub handle_end {
my ($expat, $element) = @_;
if ($element eq 'scmsync') {
my $project = $current_package_attrs->{project};
my $name = $current_package_attrs->{name};
my $scmsync = $scmsync_content;
# Use checks
$project = '' unless defined $project;
$name = '' unless defined $name;
$scmsync = '' unless defined $scmsync;
# Trim
$project =~ s/^\s+|\s+$//g;
$name =~ s/^\s+|\s+$//g;
$scmsync =~ s/^\s+|\s+$//g;
my $has_error = 0;
foreach my $val ($project, $name, $scmsync) {
if ($val =~ /\s/) {
print STDERR "Error: Value '$val' contains whitespace.\n";
$has_error = 1;
}
}
unless ($has_error) {
if ($devel_pkgs{"$name $project"}) {
print "$name $project $scmsync\n";
}
}
}
# Reset current element if we are closing it
if ($current_element eq $element) {
$current_element = '';
}
}

View File

@@ -1,65 +1,41 @@
Group Review Bot
================
This workaround is mainly needed because Gitea does not track which team member performed a review on behalf of a team.
Areas of responsibility
-----------------------
Main Tasks
----------
1. Is used to handle reviews associated with groups defined in the
ProjectGit.
Awaits a comment in the format “@groupreviewbot-name: approve”, then approves the PR with the comment “<user> approved a review on behalf of <groupreviewbot-name>.”
2. Assumes: workflow-pr needs to associate and define the PR set from
which the groups.json is read (Base of the PrjGit PR)
Target Usage
------------
Projects where policy reviews are required.
Configuration
Configiuration
--------------
The bot is configured via the `ReviewGroups` field in the `workflow.config` file, located in the ProjectGit repository.
Groups are defined in the workflow.config inside the project git. They take following options,
See `ReviewGroups` in the [workflow-pr configuration](../workflow-pr/README.md#config-file).
```json
{
...
"ReviewGroups": [
{
"Name": "name of the group user",
"Reviewers": ["members", "of", "group"],
"Silent": "(true, false) -- if true, do not explicitly require review requests of group members"
}
],
...
...
ReviewGroups: [
{
"Name": "name of the group user",
"Reviewers": ["members", "of", "group"],
"Silent": (true, false) -- if true, do not explicitly require review requests of group members
},
],
...
}
```
Server configuration
--------------------------
**Configuration file:**
| Field | Type | Notes |
| ----- | ----- | ----- |
| root | Array of string | Format **org/repo\#branch** |
Requirements
------------
Gitea token with following permissions:
- R/W PullRequest
- R/W Notification
- R User
Env Variables
-------------
The following variables can be used (and override) command line parameters.
* `AUTOGITS_CONFIG` - config file location
* `AUTOGITS_URL` - Gitea URL
* `AUTOGITS_RABBITURL` - RabbitMQ url
* `AUTOGITS_DEBUG` - when set, debug level logging enabled
Authentication env variables
* `GITEA_TOKEN` - Gitea user token
* `AMQP_USERNAME`, `AMQP_PASSWORD` - username and password for rabbitmq
* Gitea token to:
+ R/W PullRequest
+ R/W Notification
+ R User

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"log"
"net/url"
"os"
"regexp"
"runtime/debug"
"slices"
@@ -18,23 +17,20 @@ import (
"src.opensuse.org/autogits/common/gitea-generated/models"
)
type ReviewBot struct {
configs common.AutogitConfigs
acceptRx *regexp.Regexp
rejectRx *regexp.Regexp
groupName string
gitea common.Gitea
var configs common.AutogitConfigs
var acceptRx *regexp.Regexp
var rejectRx *regexp.Regexp
var groupName string
func InitRegex(newGroupName string) {
groupName = newGroupName
acceptRx = regexp.MustCompile("^:\\s*(LGTM|approved?)")
rejectRx = regexp.MustCompile("^:\\s*")
}
func (bot *ReviewBot) InitRegex(newGroupName string) {
bot.groupName = newGroupName
bot.acceptRx = regexp.MustCompile("^:\\s*(LGTM|approved?)")
bot.rejectRx = regexp.MustCompile("^:\\s*")
}
func (bot *ReviewBot) ParseReviewLine(reviewText string) (bool, string) {
func ParseReviewLine(reviewText string) (bool, string) {
line := strings.TrimSpace(reviewText)
groupTextName := "@" + bot.groupName
groupTextName := "@" + groupName
glen := len(groupTextName)
if len(line) < glen || line[0:glen] != groupTextName {
return false, line
@@ -54,20 +50,20 @@ func (bot *ReviewBot) ParseReviewLine(reviewText string) (bool, string) {
return false, line
}
func (bot *ReviewBot) ReviewAccepted(reviewText string) bool {
func ReviewAccepted(reviewText string) bool {
for _, line := range common.SplitStringNoEmpty(reviewText, "\n") {
if matched, reviewLine := bot.ParseReviewLine(line); matched {
return bot.acceptRx.MatchString(reviewLine)
if matched, reviewLine := ParseReviewLine(line); matched {
return acceptRx.MatchString(reviewLine)
}
}
return false
}
func (bot *ReviewBot) ReviewRejected(reviewText string) bool {
func ReviewRejected(reviewText string) bool {
for _, line := range common.SplitStringNoEmpty(reviewText, "\n") {
if matched, reviewLine := bot.ParseReviewLine(line); matched {
if bot.rejectRx.MatchString(reviewLine) {
return !bot.acceptRx.MatchString(reviewLine)
if matched, reviewLine := ParseReviewLine(line); matched {
if rejectRx.MatchString(reviewLine) {
return !acceptRx.MatchString(reviewLine)
}
}
}
@@ -117,10 +113,10 @@ var commentStrings = []string{
"change_time_estimate",
}*/
func (bot *ReviewBot) FindAcceptableReviewInTimeline(user string, timeline []*models.TimelineComment, reviews []*models.PullReview) *models.TimelineComment {
func FindAcceptableReviewInTimeline(user string, timeline []*models.TimelineComment, reviews []*models.PullReview) *models.TimelineComment {
for _, t := range timeline {
if t.Type == common.TimelineCommentType_Comment && t.User.UserName == user && t.Created == t.Updated {
if bot.ReviewAccepted(t.Body) || bot.ReviewRejected(t.Body) {
if ReviewAccepted(t.Body) || ReviewRejected(t.Body) {
return t
}
}
@@ -129,9 +125,9 @@ func (bot *ReviewBot) FindAcceptableReviewInTimeline(user string, timeline []*mo
return nil
}
func (bot *ReviewBot) FindOurLastReviewInTimeline(timeline []*models.TimelineComment) *models.TimelineComment {
func FindOurLastReviewInTimeline(timeline []*models.TimelineComment) *models.TimelineComment {
for _, t := range timeline {
if t.Type == common.TimelineCommentType_Review && t.User.UserName == bot.groupName && t.Created == t.Updated {
if t.Type == common.TimelineCommentType_Review && t.User.UserName == groupName && t.Created == t.Updated {
return t
}
}
@@ -139,13 +135,13 @@ func (bot *ReviewBot) FindOurLastReviewInTimeline(timeline []*models.TimelineCom
return nil
}
func (bot *ReviewBot) UnrequestReviews(org, repo string, id int64, users []string) {
if err := bot.gitea.UnrequestReview(org, repo, id, users...); err != nil {
func UnrequestReviews(gitea common.Gitea, org, repo string, id int64, users []string) {
if err := gitea.UnrequestReview(org, repo, id, users...); err != nil {
common.LogError("Can't remove reviewrs after a review:", err)
}
}
func (bot *ReviewBot) ProcessNotifications(notification *models.NotificationThread) {
func ProcessNotifications(notification *models.NotificationThread, gitea common.Gitea) {
defer func() {
if r := recover(); r != nil {
common.LogInfo("panic cought --- recovered")
@@ -172,14 +168,14 @@ func (bot *ReviewBot) ProcessNotifications(notification *models.NotificationThre
id, _ := strconv.ParseInt(match[3], 10, 64)
common.LogInfo("processing:", fmt.Sprintf("%s/%s!%d", org, repo, id))
pr, err := bot.gitea.GetPullRequest(org, repo, id)
pr, err := gitea.GetPullRequest(org, repo, id)
if err != nil {
common.LogError(" ** Cannot fetch PR associated with review:", subject.URL, "Error:", err)
return
}
if err := bot.ProcessPR(pr); err == nil && !common.IsDryRun {
if err := bot.gitea.SetNotificationRead(notification.ID); err != nil {
if err := ProcessPR(pr); err == nil && !common.IsDryRun {
if err := gitea.SetNotificationRead(notification.ID); err != nil {
common.LogDebug(" Cannot set notification as read", err)
}
} else if err != nil && err != ReviewNotFinished {
@@ -189,24 +185,24 @@ func (bot *ReviewBot) ProcessNotifications(notification *models.NotificationThre
var ReviewNotFinished = fmt.Errorf("Review is not finished")
func (bot *ReviewBot) ProcessPR(pr *models.PullRequest) error {
func ProcessPR(pr *models.PullRequest) error {
org := pr.Base.Repo.Owner.UserName
repo := pr.Base.Repo.Name
id := pr.Index
found := false
for _, reviewer := range pr.RequestedReviewers {
if reviewer != nil && reviewer.UserName == bot.groupName {
if reviewer != nil && reviewer.UserName == groupName {
found = true
break
}
}
if !found {
common.LogInfo(" review is not requested for", bot.groupName)
common.LogInfo(" review is not requested for", groupName)
return nil
}
config := bot.configs.GetPrjGitConfig(org, repo, pr.Base.Name)
config := configs.GetPrjGitConfig(org, repo, pr.Base.Name)
if config == nil {
return fmt.Errorf("Cannot find config for: %s", pr.URL)
}
@@ -216,17 +212,17 @@ func (bot *ReviewBot) ProcessPR(pr *models.PullRequest) error {
return nil
}
reviews, err := bot.gitea.GetPullRequestReviews(org, repo, id)
reviews, err := gitea.GetPullRequestReviews(org, repo, id)
if err != nil {
return fmt.Errorf("Failed to fetch reviews for: %v: %w", pr.URL, err)
}
timeline, err := common.FetchTimelineSinceReviewRequestOrPush(bot.gitea, bot.groupName, pr.Head.Sha, org, repo, id)
timeline, err := common.FetchTimelineSinceReviewRequestOrPush(gitea, groupName, pr.Head.Sha, org, repo, id)
if err != nil {
return fmt.Errorf("Failed to fetch timeline to review. %w", err)
}
groupConfig, err := config.GetReviewGroup(bot.groupName)
groupConfig, err := config.GetReviewGroup(groupName)
if err != nil {
return fmt.Errorf("Failed to fetch review group. %w", err)
}
@@ -237,30 +233,30 @@ func (bot *ReviewBot) ProcessPR(pr *models.PullRequest) error {
// pr.Head.Sha
for _, reviewer := range requestReviewers {
if review := bot.FindAcceptableReviewInTimeline(reviewer, timeline, reviews); review != nil {
if bot.ReviewAccepted(review.Body) {
if review := FindAcceptableReviewInTimeline(reviewer, timeline, reviews); review != nil {
if ReviewAccepted(review.Body) {
if !common.IsDryRun {
text := reviewer + " approved a review on behalf of " + bot.groupName
if review := bot.FindOurLastReviewInTimeline(timeline); review == nil || review.Body != text {
_, err := bot.gitea.AddReviewComment(pr, common.ReviewStateApproved, text)
text := reviewer + " approved a review on behalf of " + groupName
if review := FindOurLastReviewInTimeline(timeline); review == nil || review.Body != text {
_, err := gitea.AddReviewComment(pr, common.ReviewStateApproved, text)
if err != nil {
common.LogError(" -> failed to write approval comment", err)
}
bot.UnrequestReviews(org, repo, id, requestReviewers)
UnrequestReviews(gitea, org, repo, id, requestReviewers)
}
}
common.LogInfo(" -> approved by", reviewer)
common.LogInfo(" review at", review.Created)
return nil
} else if bot.ReviewRejected(review.Body) {
} else if ReviewRejected(review.Body) {
if !common.IsDryRun {
text := reviewer + " requested changes on behalf of " + bot.groupName + ". See " + review.HTMLURL
if review := bot.FindOurLastReviewInTimeline(timeline); review == nil || review.Body != text {
_, err := bot.gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, text)
text := reviewer + " requested changes on behalf of " + groupName + ". See " + review.HTMLURL
if review := FindOurLastReviewInTimeline(timeline); review == nil || review.Body != text {
_, err := gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Changes requested. See review by: "+reviewer)
if err != nil {
common.LogError(" -> failed to write rejecting comment", err)
}
bot.UnrequestReviews(org, repo, id, requestReviewers)
UnrequestReviews(gitea, org, repo, id, requestReviewers)
}
}
common.LogInfo(" -> declined by", reviewer)
@@ -274,7 +270,7 @@ func (bot *ReviewBot) ProcessPR(pr *models.PullRequest) error {
if !groupConfig.Silent && len(requestReviewers) > 0 {
common.LogDebug(" Requesting reviews for:", requestReviewers)
if !common.IsDryRun {
if _, err := bot.gitea.RequestReviews(pr, requestReviewers...); err != nil {
if _, err := gitea.RequestReviews(pr, requestReviewers...); err != nil {
common.LogDebug(" -> err:", err)
}
} else {
@@ -287,40 +283,42 @@ func (bot *ReviewBot) ProcessPR(pr *models.PullRequest) error {
// add a helpful comment, if not yet added
found_help_comment := false
for _, t := range timeline {
if t.Type == common.TimelineCommentType_Comment && t.User != nil && t.User.UserName == bot.groupName {
if t.Type == common.TimelineCommentType_Comment && t.User != nil && t.User.UserName == groupName {
found_help_comment = true
break
}
}
if !found_help_comment && !common.IsDryRun {
helpComment := fmt.Sprintln("Review by", bot.groupName, "represents a group of reviewers:", strings.Join(requestReviewers, ", "), ".\n\n"+
helpComment := fmt.Sprintln("Review by", groupName, "represents a group of reviewers:", strings.Join(requestReviewers, ", "), ".\n\n"+
"Do **not** use standard review interface to review on behalf of the group.\n"+
"To accept the review on behalf of the group, create the following comment: `@"+bot.groupName+": approve`.\n"+
"To request changes on behalf of the group, create the following comment: `@"+bot.groupName+": decline` followed with lines justifying the decision.\n"+
"To accept the review on behalf of the group, create the following comment: `@"+groupName+": approve`.\n"+
"To request changes on behalf of the group, create the following comment: `@"+groupName+": decline` followed with lines justifying the decision.\n"+
"Future edits of the comments are ignored, a new comment is required to change the review state.")
if slices.Contains(groupConfig.Reviewers, pr.User.UserName) {
helpComment = helpComment + "\n\n" +
"Submitter is member of this review group, hence they are excluded from being one of the reviewers here"
}
bot.gitea.AddComment(pr, helpComment)
gitea.AddComment(pr, helpComment)
}
return ReviewNotFinished
}
func (bot *ReviewBot) PeriodReviewCheck() {
notifications, err := bot.gitea.GetNotifications(common.GiteaNotificationType_Pull, nil)
func PeriodReviewCheck() {
notifications, err := gitea.GetNotifications(common.GiteaNotificationType_Pull, nil)
if err != nil {
common.LogError(" Error fetching unread notifications: %w", err)
return
}
for _, notification := range notifications {
bot.ProcessNotifications(notification)
ProcessNotifications(notification, gitea)
}
}
var gitea common.Gitea
func main() {
giteaUrl := flag.String("gitea-url", "https://src.opensuse.org", "Gitea instance used for reviews")
rabbitMqHost := flag.String("rabbit-url", "amqps://rabbit.opensuse.org", "RabbitMQ instance where Gitea webhook notifications are sent")
@@ -330,24 +328,6 @@ func main() {
flag.BoolVar(&common.IsDryRun, "dry", false, "Dry run, no effect. For debugging")
flag.Parse()
if err := common.SetLoggingLevelFromString(*logging); err != nil {
common.LogError(err.Error())
return
}
if cf := os.Getenv("AUTOGITS_CONFIG"); len(cf) > 0 {
*configFile = cf
}
if url := os.Getenv("AUTOGITS_URL"); len(url) > 0 {
*giteaUrl = url
}
if url := os.Getenv("AUTOGITS_RABBITURL"); len(url) > 0 {
*rabbitMqHost = url
}
if debug := os.Getenv("AUTOGITS_DEBUG"); len(debug) > 0 {
common.SetLoggingLevel(common.LogLevelDebug)
}
args := flag.Args()
if len(args) != 1 {
log.Println(" syntax:")
@@ -356,7 +336,7 @@ func main() {
flag.Usage()
return
}
targetGroupName := args[0]
groupName = args[0]
if *configFile == "" {
common.LogError("Missing config file")
@@ -379,35 +359,36 @@ func main() {
return
}
giteaTransport := common.AllocateGiteaTransport(*giteaUrl)
configs, err := common.ResolveWorkflowConfigs(giteaTransport, configData)
gitea = common.AllocateGiteaTransport(*giteaUrl)
configs, err = common.ResolveWorkflowConfigs(gitea, configData)
if err != nil {
common.LogError("Cannot parse workflow configs:", err)
return
}
reviewer, err := giteaTransport.GetCurrentUser()
reviewer, err := gitea.GetCurrentUser()
if err != nil {
common.LogError("Cannot fetch review user:", err)
return
}
if err := common.SetLoggingLevelFromString(*logging); err != nil {
common.LogError(err.Error())
return
}
if *interval < 1 {
*interval = 1
}
bot := &ReviewBot{
gitea: giteaTransport,
configs: configs,
}
bot.InitRegex(targetGroupName)
InitRegex(groupName)
common.LogInfo(" ** processing group reviews for group:", bot.groupName)
common.LogInfo(" ** processing group reviews for group:", groupName)
common.LogInfo(" ** username in Gitea:", reviewer.UserName)
common.LogInfo(" ** polling interval:", *interval, "min")
common.LogInfo(" ** connecting to RabbitMQ:", *rabbitMqHost)
if bot.groupName != reviewer.UserName {
if groupName != reviewer.UserName {
common.LogError(" ***** Reviewer does not match group name. Aborting. *****")
return
}
@@ -419,13 +400,10 @@ func main() {
}
config_update := ConfigUpdatePush{
bot: bot,
config_modified: make(chan *common.AutogitConfig),
}
process_issue_pr := IssueCommentProcessor{
bot: bot,
}
process_issue_pr := IssueCommentProcessor{}
configUpdates := &common.RabbitMQGiteaEventsProcessor{
Orgs: []string{},
@@ -435,7 +413,7 @@ func main() {
},
}
configUpdates.Connection().RabbitURL = u
for _, c := range bot.configs {
for _, c := range configs {
if org, _, _ := c.GetPrjGit(); !slices.Contains(configUpdates.Orgs, org) {
configUpdates.Orgs = append(configUpdates.Orgs, org)
}
@@ -448,17 +426,17 @@ func main() {
select {
case configTouched, ok := <-config_update.config_modified:
if ok {
for idx, c := range bot.configs {
for idx, c := range configs {
if c == configTouched {
org, repo, branch := c.GetPrjGit()
prj := fmt.Sprintf("%s/%s#%s", org, repo, branch)
common.LogInfo("Detected config update for", prj)
new_config, err := common.ReadWorkflowConfig(bot.gitea, prj)
new_config, err := common.ReadWorkflowConfig(gitea, prj)
if err != nil {
common.LogError("Failed parsing Project config for", prj, err)
} else {
bot.configs[idx] = new_config
configs[idx] = new_config
}
}
}
@@ -468,7 +446,7 @@ func main() {
}
}
bot.PeriodReviewCheck()
PeriodReviewCheck()
time.Sleep(time.Duration(*interval * int64(time.Minute)))
}
}

View File

@@ -1,359 +1,6 @@
package main
import (
"fmt"
"testing"
"go.uber.org/mock/gomock"
"src.opensuse.org/autogits/common"
"src.opensuse.org/autogits/common/gitea-generated/models"
mock_common "src.opensuse.org/autogits/common/mock"
)
func TestProcessPR(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockGitea := mock_common.NewMockGitea(ctrl)
groupName := "testgroup"
bot := &ReviewBot{
gitea: mockGitea,
groupName: groupName,
}
bot.InitRegex(groupName)
org := "myorg"
repo := "myrepo"
prIndex := int64(1)
headSha := "abcdef123456"
pr := &models.PullRequest{
Index: prIndex,
URL: "http://gitea/pr/1",
State: "open",
Base: &models.PRBranchInfo{
Name: "main",
Repo: &models.Repository{
Name: repo,
Owner: &models.User{
UserName: org,
},
},
},
Head: &models.PRBranchInfo{
Sha: headSha,
},
User: &models.User{
UserName: "submitter",
},
RequestedReviewers: []*models.User{
{UserName: groupName},
},
}
prjConfig := &common.AutogitConfig{
GitProjectName: org + "/" + repo + "#main",
ReviewGroups: []*common.ReviewGroup{
{
Name: groupName,
Reviewers: []string{"reviewer1", "reviewer2"},
},
},
}
bot.configs = common.AutogitConfigs{prjConfig}
t.Run("Review not requested for group", func(t *testing.T) {
prNoRequest := *pr
prNoRequest.RequestedReviewers = nil
err := bot.ProcessPR(&prNoRequest)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
})
t.Run("PR is closed", func(t *testing.T) {
prClosed := *pr
prClosed.State = "closed"
err := bot.ProcessPR(&prClosed)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
})
t.Run("Successful Approval", func(t *testing.T) {
common.IsDryRun = false
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
// reviewer1 approved in timeline
timeline := []*models.TimelineComment{
{
Type: common.TimelineCommentType_Comment,
User: &models.User{UserName: "reviewer1"},
Body: "@" + groupName + ": approve",
},
}
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
expectedText := "reviewer1 approved a review on behalf of " + groupName
mockGitea.EXPECT().AddReviewComment(pr, common.ReviewStateApproved, expectedText).Return(nil, nil)
mockGitea.EXPECT().UnrequestReview(org, repo, prIndex, gomock.Any()).Return(nil)
err := bot.ProcessPR(pr)
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}
})
t.Run("Dry Run - No actions taken", func(t *testing.T) {
common.IsDryRun = true
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
timeline := []*models.TimelineComment{
{
Type: common.TimelineCommentType_Comment,
User: &models.User{UserName: "reviewer1"},
Body: "@" + groupName + ": approve",
},
}
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
// No AddReviewComment or UnrequestReview should be called
err := bot.ProcessPR(pr)
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}
})
t.Run("Approval already exists - No new comment", func(t *testing.T) {
common.IsDryRun = false
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
approvalText := "reviewer1 approved a review on behalf of " + groupName
timeline := []*models.TimelineComment{
{
Type: common.TimelineCommentType_Review,
User: &models.User{UserName: groupName},
Body: approvalText,
},
{
Type: common.TimelineCommentType_Comment,
User: &models.User{UserName: "reviewer1"},
Body: "@" + groupName + ": approve",
},
{
Type: common.TimelineCommentType_Comment,
User: &models.User{UserName: groupName},
Body: "Help comment",
},
}
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
// No AddReviewComment, UnrequestReview, or AddComment should be called
err := bot.ProcessPR(pr)
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}
})
t.Run("Rejection already exists - No new comment", func(t *testing.T) {
common.IsDryRun = false
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
rejectionText := "reviewer1 requested changes on behalf of " + groupName + ". See http://gitea/comment/123"
timeline := []*models.TimelineComment{
{
Type: common.TimelineCommentType_Review,
User: &models.User{UserName: groupName},
Body: rejectionText,
},
{
Type: common.TimelineCommentType_Comment,
User: &models.User{UserName: "reviewer1"},
Body: "@" + groupName + ": decline",
HTMLURL: "http://gitea/comment/123",
},
{
Type: common.TimelineCommentType_Comment,
User: &models.User{UserName: groupName},
Body: "Help comment",
},
}
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
err := bot.ProcessPR(pr)
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}
})
t.Run("Pending review - Help comment already exists", func(t *testing.T) {
common.IsDryRun = false
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
timeline := []*models.TimelineComment{
{
Type: common.TimelineCommentType_Comment,
User: &models.User{UserName: groupName},
Body: "Some help comment",
},
}
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
// It will try to request reviews
mockGitea.EXPECT().RequestReviews(pr, "reviewer1", "reviewer2").Return(nil, nil)
// AddComment should NOT be called because bot already has a comment in timeline
err := bot.ProcessPR(pr)
if err != ReviewNotFinished {
t.Errorf("Expected ReviewNotFinished error, got %v", err)
}
})
t.Run("Submitter is group member - Excluded from review request", func(t *testing.T) {
common.IsDryRun = false
prSubmitterMember := *pr
prSubmitterMember.User = &models.User{UserName: "reviewer1"}
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(nil, nil)
mockGitea.EXPECT().RequestReviews(&prSubmitterMember, "reviewer2").Return(nil, nil)
mockGitea.EXPECT().AddComment(&prSubmitterMember, gomock.Any()).Return(nil)
err := bot.ProcessPR(&prSubmitterMember)
if err != ReviewNotFinished {
t.Errorf("Expected ReviewNotFinished error, got %v", err)
}
})
t.Run("Successful Rejection", func(t *testing.T) {
common.IsDryRun = false
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
timeline := []*models.TimelineComment{
{
Type: common.TimelineCommentType_Comment,
User: &models.User{UserName: "reviewer2"},
Body: "@" + groupName + ": decline",
HTMLURL: "http://gitea/comment/999",
},
}
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
expectedText := "reviewer2 requested changes on behalf of " + groupName + ". See http://gitea/comment/999"
mockGitea.EXPECT().AddReviewComment(pr, common.ReviewStateRequestChanges, expectedText).Return(nil, nil)
mockGitea.EXPECT().UnrequestReview(org, repo, prIndex, gomock.Any()).Return(nil)
err := bot.ProcessPR(pr)
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}
})
t.Run("Config not found", func(t *testing.T) {
bot.configs = common.AutogitConfigs{}
err := bot.ProcessPR(pr)
if err == nil {
t.Error("Expected error when config is missing, got nil")
}
})
t.Run("Gitea error in GetPullRequestReviews", func(t *testing.T) {
bot.configs = common.AutogitConfigs{prjConfig}
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, fmt.Errorf("gitea error"))
err := bot.ProcessPR(pr)
if err == nil {
t.Error("Expected error from gitea, got nil")
}
})
}
func TestProcessNotifications(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockGitea := mock_common.NewMockGitea(ctrl)
groupName := "testgroup"
bot := &ReviewBot{
gitea: mockGitea,
groupName: groupName,
}
bot.InitRegex(groupName)
org := "myorg"
repo := "myrepo"
prIndex := int64(123)
notificationID := int64(456)
notification := &models.NotificationThread{
ID: notificationID,
Subject: &models.NotificationSubject{
URL: fmt.Sprintf("http://gitea/api/v1/repos/%s/%s/pulls/%d", org, repo, prIndex),
},
}
t.Run("Notification Success", func(t *testing.T) {
common.IsDryRun = false
pr := &models.PullRequest{
Index: prIndex,
Base: &models.PRBranchInfo{
Name: "main",
Repo: &models.Repository{
Name: repo,
Owner: &models.User{UserName: org},
},
},
Head: &models.PRBranchInfo{
Sha: "headsha",
Repo: &models.Repository{
Name: repo,
Owner: &models.User{UserName: org},
},
},
User: &models.User{UserName: "submitter"},
RequestedReviewers: []*models.User{{UserName: groupName}},
}
mockGitea.EXPECT().GetPullRequest(org, repo, prIndex).Return(pr, nil)
prjConfig := &common.AutogitConfig{
GitProjectName: org + "/" + repo + "#main",
ReviewGroups: []*common.ReviewGroup{{Name: groupName, Reviewers: []string{"r1"}}},
}
bot.configs = common.AutogitConfigs{prjConfig}
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
timeline := []*models.TimelineComment{
{
Type: common.TimelineCommentType_Comment,
User: &models.User{UserName: "r1"},
Body: "@" + groupName + ": approve",
},
}
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
expectedText := "r1 approved a review on behalf of " + groupName
mockGitea.EXPECT().AddReviewComment(pr, common.ReviewStateApproved, expectedText).Return(nil, nil)
mockGitea.EXPECT().UnrequestReview(org, repo, prIndex, gomock.Any()).Return(nil)
mockGitea.EXPECT().SetNotificationRead(notificationID).Return(nil)
bot.ProcessNotifications(notification)
})
t.Run("Invalid Notification URL", func(t *testing.T) {
badNotification := &models.NotificationThread{
Subject: &models.NotificationSubject{
URL: "http://gitea/invalid/url",
},
}
bot.ProcessNotifications(badNotification)
})
t.Run("Gitea error in GetPullRequest", func(t *testing.T) {
mockGitea.EXPECT().GetPullRequest(org, repo, prIndex).Return(nil, fmt.Errorf("gitea error"))
bot.ProcessNotifications(notification)
})
}
import "testing"
func TestReviewApprovalCheck(t *testing.T) {
tests := []struct {
@@ -413,78 +60,16 @@ func TestReviewApprovalCheck(t *testing.T) {
InString: "@group2: disapprove",
Rejected: true,
},
{
Name: "Whitespace before colon",
GroupName: "group",
InString: "@group : LGTM",
Approved: true,
},
{
Name: "No whitespace after colon",
GroupName: "group",
InString: "@group:LGTM",
Approved: true,
},
{
Name: "Leading and trailing whitespace on line",
GroupName: "group",
InString: " @group: LGTM ",
Approved: true,
},
{
Name: "Multiline: Approved on second line",
GroupName: "group",
InString: "Random noise\n@group: approved",
Approved: true,
},
{
Name: "Multiline: Multiple group mentions, first wins",
GroupName: "group",
InString: "@group: decline\n@group: approve",
Rejected: true,
},
{
Name: "Multiline: Approved on second line",
GroupName: "group",
InString: "noise\n@group: approve\nmore noise",
Approved: true,
},
{
Name: "Not at start of line (even with whitespace)",
GroupName: "group",
InString: "Hello @group: approve",
Approved: false,
},
{
Name: "Rejecting with reason",
GroupName: "group",
InString: "@group: decline because of X, Y and Z",
Rejected: true,
},
{
Name: "No colon after group",
GroupName: "group",
InString: "@group LGTM",
Approved: false,
Rejected: false,
},
{
Name: "Invalid char after group",
GroupName: "group",
InString: "@group! LGTM",
Approved: false,
Rejected: false,
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
bot := &ReviewBot{}
bot.InitRegex(test.GroupName)
InitRegex(test.GroupName)
if r := bot.ReviewAccepted(test.InString); r != test.Approved {
if r := ReviewAccepted(test.InString); r != test.Approved {
t.Error("ReviewAccepted() returned", r, "expecting", test.Approved)
}
if r := bot.ReviewRejected(test.InString); r != test.Rejected {
if r := ReviewRejected(test.InString); r != test.Rejected {
t.Error("ReviewRejected() returned", r, "expecting", test.Rejected)
}
})

View File

@@ -7,9 +7,7 @@ import (
"src.opensuse.org/autogits/common"
)
type IssueCommentProcessor struct {
bot *ReviewBot
}
type IssueCommentProcessor struct{}
func (s *IssueCommentProcessor) ProcessFunc(req *common.Request) error {
if req.Type != common.RequestType_IssueComment {
@@ -21,15 +19,14 @@ func (s *IssueCommentProcessor) ProcessFunc(req *common.Request) error {
repo := data.Repository.Name
index := int64(data.Issue.Number)
pr, err := s.bot.gitea.GetPullRequest(org, repo, index)
pr, err := gitea.GetPullRequest(org, repo, index)
if err != nil {
return fmt.Errorf("Failed to fetch PullRequest from event: %s/%s!%d Error: %w", org, repo, index, err)
}
return s.bot.ProcessPR(pr)
return ProcessPR(pr)
}
type ConfigUpdatePush struct {
bot *ReviewBot
config_modified chan *common.AutogitConfig
}
@@ -49,7 +46,7 @@ func (s *ConfigUpdatePush) ProcessFunc(req *common.Request) error {
}
branch := data.Ref[len(branch_ref):]
c := s.bot.configs.GetPrjGitConfig(org, repo, branch)
c := configs.GetPrjGitConfig(org, repo, branch)
if c == nil {
return nil
}
@@ -67,7 +64,7 @@ func (s *ConfigUpdatePush) ProcessFunc(req *common.Request) error {
}
if modified_config {
for _, config := range s.bot.configs {
for _, config := range configs {
if o, r, _ := config.GetPrjGit(); o == org && r == repo {
s.config_modified <- config
}

View File

@@ -1,203 +0,0 @@
package main
import (
"fmt"
"testing"
"go.uber.org/mock/gomock"
"src.opensuse.org/autogits/common"
"src.opensuse.org/autogits/common/gitea-generated/models"
mock_common "src.opensuse.org/autogits/common/mock"
)
func TestIssueCommentProcessor(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockGitea := mock_common.NewMockGitea(ctrl)
groupName := "testgroup"
bot := &ReviewBot{
gitea: mockGitea,
groupName: groupName,
}
bot.InitRegex(groupName)
processor := &IssueCommentProcessor{bot: bot}
org := "myorg"
repo := "myrepo"
index := 123
event := &common.IssueCommentWebhookEvent{
Repository: &common.Repository{
Name: repo,
Owner: &common.Organization{
Username: org,
},
},
Issue: &common.IssueDetail{
Number: index,
},
}
req := &common.Request{
Type: common.RequestType_IssueComment,
Data: event,
}
t.Run("Successful Processing", func(t *testing.T) {
pr := &models.PullRequest{
Index: int64(index),
Base: &models.PRBranchInfo{
Name: "main",
Repo: &models.Repository{
Name: repo,
Owner: &models.User{UserName: org},
},
},
Head: &models.PRBranchInfo{
Sha: "headsha",
Repo: &models.Repository{
Name: repo,
Owner: &models.User{UserName: org},
},
},
User: &models.User{UserName: "submitter"},
RequestedReviewers: []*models.User{{UserName: groupName}},
}
mockGitea.EXPECT().GetPullRequest(org, repo, int64(index)).Return(pr, nil)
prjConfig := &common.AutogitConfig{
GitProjectName: org + "/" + repo + "#main",
ReviewGroups: []*common.ReviewGroup{{Name: groupName, Reviewers: []string{"r1"}}},
}
bot.configs = common.AutogitConfigs{prjConfig}
mockGitea.EXPECT().GetPullRequestReviews(org, repo, int64(index)).Return(nil, nil)
mockGitea.EXPECT().GetTimeline(org, repo, int64(index)).Return(nil, nil)
mockGitea.EXPECT().RequestReviews(pr, "r1").Return(nil, nil)
mockGitea.EXPECT().AddComment(pr, gomock.Any()).Return(nil)
err := processor.ProcessFunc(req)
if err != ReviewNotFinished {
t.Errorf("Expected ReviewNotFinished, got %v", err)
}
})
t.Run("Gitea error in GetPullRequest", func(t *testing.T) {
mockGitea.EXPECT().GetPullRequest(org, repo, int64(index)).Return(nil, fmt.Errorf("gitea error"))
err := processor.ProcessFunc(req)
if err == nil {
t.Error("Expected error, got nil")
}
})
t.Run("Wrong Request Type", func(t *testing.T) {
wrongReq := &common.Request{Type: common.RequestType_Push}
err := processor.ProcessFunc(wrongReq)
if err == nil {
t.Error("Expected error for wrong request type, got nil")
}
})
}
func TestConfigUpdatePush(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
groupName := "testgroup"
bot := &ReviewBot{
groupName: groupName,
}
bot.InitRegex(groupName)
configChan := make(chan *common.AutogitConfig, 1)
processor := &ConfigUpdatePush{
bot: bot,
config_modified: configChan,
}
org := "myorg"
repo := "myrepo"
branch := "main"
prjConfig := &common.AutogitConfig{
GitProjectName: org + "/" + repo + "#" + branch,
Organization: org,
Branch: branch,
}
bot.configs = common.AutogitConfigs{prjConfig}
event := &common.PushWebhookEvent{
Ref: "refs/heads/" + branch,
Repository: &common.Repository{
Name: repo,
Owner: &common.Organization{
Username: org,
},
},
Commits: []common.Commit{
{
Modified: []string{common.ProjectConfigFile},
},
},
}
req := &common.Request{
Type: common.RequestType_Push,
Data: event,
}
t.Run("Config Modified", func(t *testing.T) {
err := processor.ProcessFunc(req)
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}
select {
case modified := <-configChan:
if modified != prjConfig {
t.Errorf("Expected modified config to be %v, got %v", prjConfig, modified)
}
default:
t.Error("Expected config modification signal, but none received")
}
})
t.Run("No Config Modified", func(t *testing.T) {
noConfigEvent := *event
noConfigEvent.Commits = []common.Commit{{Modified: []string{"README.md"}}}
noConfigReq := &common.Request{Type: common.RequestType_Push, Data: &noConfigEvent}
err := processor.ProcessFunc(noConfigReq)
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}
select {
case <-configChan:
t.Error("Did not expect config modification signal")
default:
// Success
}
})
t.Run("Wrong Branch Ref", func(t *testing.T) {
wrongBranchEvent := *event
wrongBranchEvent.Ref = "refs/tags/v1.0"
wrongBranchReq := &common.Request{Type: common.RequestType_Push, Data: &wrongBranchEvent}
err := processor.ProcessFunc(wrongBranchReq)
if err == nil {
t.Error("Expected error for wrong branch ref, got nil")
}
})
t.Run("Config Not Found", func(t *testing.T) {
bot.configs = common.AutogitConfigs{}
err := processor.ProcessFunc(req)
if err != nil {
t.Errorf("Expected nil error even if config not found, got %v", err)
}
})
}

View File

@@ -4,15 +4,11 @@ OBS Staging Bot
Build a PR against a ProjectGit, if review is requested.
Main Tasks
----------
Areas of Responsibility
-----------------------
* A build in OBS is initiated when a review for this bot is requested.
* The overall build status is reported:
* Build successful
* Build failed
* It checks the build status only for the involved packages compared to the last state of the project for all architectures and all flavors.
* It adds an svg with detailed building status.
* Monitors Notification API in Gitea for review requests
* Reviews Package build results in OBS for all changed packages in ProjectGit PR
Target Usage
@@ -25,48 +21,22 @@ Configuration File
------------------
Bot reads `staging.config` from the project git or the PR to the project git.
It's a JSON file with following syntax:
It's a JSON file with following syntax
```json
```
{
"ObsProject": "SUSE:SLFO:1.2",
"StagingProject": "SUSE:SLFO:1.2:PullRequest",
"QA": [
{
"Name": "SLES",
"Origin": "SUSE:SLFO:Products:SLES:16.0",
"BuildDisableRepos": ["product"]
}
]
"ObsProject": "home:foo:project",
"StagingProject": "home:foo:project:staging",
"QA": [
{
"Name": "ProjectBuild",
"Origin": "home:foo:product:images"
}
]
}
```
| Field name | Details | Mandatory | Type | Allowed Values | Default |
| ----- | ----- | ----- | ----- | ----- | ----- |
| *ObsProject* | Product OBS project. Builds in this project will be used to compare to builds based on sources from the PR. | yes | string | `[a-zA-Z0-9-_:]+` | |
| *StagingProject* | Used both as base project and prefix for all OBS staging projects. Upon being added as a reviewer to a PrjGit PR, this bot automatically generates an OBS project named *StagingProject:<PR_Number>*. It must be a sub-project of the *ObsProject*. | yes | string | `[a-zA-Z0-9-_:]+` | |
| *QA* | Crucial for generating a product build (such as an ISO or FTP tree) that incorporates the packages. | no | array of objects | | |
| *QA > Name* | Suffix for the QA OBS staging project. The project is named *StagingProject:<PR_Number>:Name*. | no | string | | |
| *QA > Origin* | OBS reference project | no | string | | |
| *QA > BuildDisableRepos* | The names of OBS repositories to build-disable, if any. | no | array of strings | | [] |
Details
-------
* **OBS staging projects are deleted** when the relative PrjGit PR is closed or merged.
* **PrjGit PR - staging project**
* The OBS staging project utilizes an **scmsync** tag, configured with the `onlybuild` flag, to exclusively build packages associated with this specific PrjGit PR.
* The **build config** is inherited from the PrjGit PR config file (even if unchanged).
* The **project meta** creates a standard repository following the StagingProject as a project path.
* The base *StagingProject* has the macro **FromScratch:** set in its config, which prevents inheriting the configuration from the included project paths.
* The bot copies the project maintainers from *StagingProject* to the specific staging project (*StagingProject:<PR_Number>*).
* The bot reports “Build successful” only if the build is successful for all repositories and all architectures.
* **PrjGit PR - QA staging project**
* The QA staging project is meant for building the product; the relative build config is inherited from the `QA > Origin` project.
* In this case, the **scmsync** tag is inherited from the `QA > Origin` project.
* It is desirable in some cases to avoid building some specific build service repositories when not needed. In this case, `QA > BuildDisableRepos` can be specified.
These repositories would be disabled in the project meta when generating the QA project.
* ObsProject: (**required**) Project where the base project is built. Builds in this project will be used to compare to builds based on sources from the PR
* StagingProject: template project that will be used as template for the staging project. Omitting this will use the ObsProject repositories to create the staging. Staging project will be created under the template, or in the bot's home directory if not specified.
* QA: set of projects to build ontop of the binaries built in staging.

View File

@@ -26,6 +26,7 @@ import (
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"runtime/debug"
"slices"
@@ -109,166 +110,67 @@ const (
BuildStatusSummaryUnknown = 4
)
type DisableFlag struct {
XMLName string `xml:"disable"`
Name string `xml:"repository,attr"`
}
func ProcessBuildStatus(project, refProject *common.BuildResultList) BuildStatusSummary {
if _, finished := refProject.BuildResultSummary(); !finished {
common.LogDebug("refProject not finished building??")
return BuildStatusSummaryUnknown
}
func ProcessBuildStatus(project *common.BuildResultList) BuildStatusSummary {
if _, finished := project.BuildResultSummary(); !finished {
common.LogDebug("Still building...")
return BuildStatusSummaryBuilding
}
// the repositories should be setup equally between the projects. We
// need to verify that packages that are building in `refProject` are not
// failing in the `project`
BuildResultSorter := func(a, b *common.BuildResult) int {
if c := strings.Compare(a.Repository, b.Repository); c != 0 {
return c
}
if c := strings.Compare(a.Arch, b.Arch); c != 0 {
return c
}
//slices.SortFunc(project.Result, BuildResultSorter)
panic("Should not happen -- BuiltResultSorter equal repos?")
}
slices.SortFunc(project.Result, BuildResultSorter)
if refProject == nil {
// just return if buid finished and have some successes, since new package
common.LogInfo("New package. Only need some success...")
SomeSuccess := false
for i := 0; i < len(project.Result); i++ {
repoRes := project.Result[i]
repoResStatus, ok := common.ObsRepoStatusDetails[repoRes.Code]
if !ok {
common.LogDebug("cannot find code:", repoRes.Code)
return BuildStatusSummaryUnknown
}
if !repoResStatus.Finished {
return BuildStatusSummaryBuilding
}
for _, pkg := range repoRes.Status {
pkgStatus, ok := common.ObsBuildStatusDetails[pkg.Code]
if !ok {
common.LogInfo("Unknown package build status:", pkg.Code, "for", pkg.Package)
common.LogDebug("Details:", pkg.Details)
}
if pkgStatus.Success {
SomeSuccess = true
}
}
}
if SomeSuccess {
return BuildStatusSummarySuccess
}
return BuildStatusSummaryFailed
}
slices.SortFunc(refProject.Result, BuildResultSorter)
common.LogDebug("comparing results", len(project.Result), "vs. ref", len(refProject.Result))
SomeSuccess := false
common.LogDebug("build results", len(project.Result))
for i := 0; i < len(project.Result); i++ {
common.LogDebug("searching for", project.Result[i].Repository, "/", project.Result[i].Arch)
j := 0
found:
for ; j < len(refProject.Result); j++ {
if project.Result[i].Repository != refProject.Result[j].Repository ||
project.Result[i].Arch != refProject.Result[j].Arch {
continue
}
for j = 0; j < len(project.Result); j++ {
common.LogDebug(" found match for @ idx:", j)
res, success := ProcessRepoBuildStatus(project.Result[i].Status, refProject.Result[j].Status)
res := ProcessRepoBuildStatus(project.Result[i].Status)
switch res {
case BuildStatusSummarySuccess:
SomeSuccess = SomeSuccess || success
break found
case BuildStatusSummaryFailed:
return BuildStatusSummaryFailed
default:
return res
}
}
if j >= len(refProject.Result) {
common.LogDebug("Cannot find results...")
common.LogDebug(project.Result[i])
common.LogDebug(refProject.Result)
return BuildStatusSummaryUnknown
}
}
if SomeSuccess {
return BuildStatusSummarySuccess
}
return BuildStatusSummaryFailed
return BuildStatusSummarySuccess
}
func ProcessRepoBuildStatus(results, ref []*common.PackageBuildStatus) (status BuildStatusSummary, SomeSuccess bool) {
PackageBuildStatusSorter := func(a, b *common.PackageBuildStatus) int {
return strings.Compare(a.Package, b.Package)
}
func ProcessRepoBuildStatus(results []*common.PackageBuildStatus) (status BuildStatusSummary) {
PackageBuildStatusSorter := func(a, b *common.PackageBuildStatus) int {
return strings.Compare(a.Package, b.Package)
}
common.LogDebug("******** REF: ")
data, _ := xml.MarshalIndent(ref, "", " ")
common.LogDebug(string(data))
common.LogDebug("******* RESULTS: ")
data, _ = xml.MarshalIndent(results, "", " ")
data, _ := xml.MarshalIndent(results, "", " ")
common.LogDebug(string(data))
common.LogDebug("*******")
// compare build result
slices.SortFunc(results, PackageBuildStatusSorter)
slices.SortFunc(ref, PackageBuildStatusSorter)
j := 0
SomeSuccess = false
for i := 0; i < len(results); i++ {
res, ok := common.ObsBuildStatusDetails[results[i].Code]
if !ok {
common.LogInfo("unknown package result code:", results[i].Code, "for package:", results[i].Package)
return BuildStatusSummaryUnknown, SomeSuccess
return BuildStatusSummaryUnknown
}
if !res.Finished {
return BuildStatusSummaryBuilding, SomeSuccess
return BuildStatusSummaryBuilding
}
if !res.Success {
// not failed if reference project also failed for same package here
for ; j < len(results) && strings.Compare(results[i].Package, ref[j].Package) < 0; j++ {
}
if j < len(results) && results[i].Package == ref[j].Package {
refRes, ok := common.ObsBuildStatusDetails[ref[j].Code]
if !ok {
common.LogInfo("unknown ref package result code:", ref[j].Code, "package:", ref[j].Package)
return BuildStatusSummaryUnknown, SomeSuccess
}
if !refRes.Finished {
common.LogDebug("Not finished building in reference project?")
}
if refRes.Success {
return BuildStatusSummaryFailed, SomeSuccess
}
}
} else {
SomeSuccess = true
return BuildStatusSummaryFailed
}
}
return BuildStatusSummarySuccess, SomeSuccess
return BuildStatusSummarySuccess
}
func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingPrj, buildPrj string, stagingMasterPrj string) (*common.ProjectMeta, error) {
@@ -327,9 +229,9 @@ func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullReque
common.LogError("error fetching project meta for", buildPrj, ". Err:", err)
return nil, err
}
common.LogInfo("Meta: ", meta)
// generate new project with paths pointinig back to original repos
// disable publishing
meta.Name = stagingPrj
meta.Description = fmt.Sprintf(`Pull request build job PR#%d to branch %s of %s/%s`,
@@ -344,7 +246,10 @@ func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullReque
urlPkg := make([]string, 0, len(modifiedOrNew))
for _, pkg := range modifiedOrNew {
urlPkg = append(urlPkg, "onlybuild="+url.QueryEscape(pkg))
// FIXME: skip manifest subdirectories itself
// strip any leading directory name and just hand over last directory as package name
onlybuilds := strings.Split(pkg, "/")
urlPkg = append(urlPkg, "onlybuild="+url.QueryEscape(onlybuilds[len(onlybuilds)-1]))
}
meta.ScmSync = pr.Head.Repo.CloneURL + "?" + strings.Join(urlPkg, "&") + "#" + pr.Head.Sha
if len(meta.ScmSync) >= 65535 {
@@ -382,7 +287,7 @@ 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(stagingConfig *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingProject, templateProject, subProjectName string) error {
common.LogDebug("Setup QA sub projects")
templateMeta, err := ObsClient.GetProjectMeta(templateProject)
if err != nil {
@@ -412,21 +317,7 @@ func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, git
repository.Fragment = branch.SHA
templateMeta.ScmSync = repository.String()
common.LogDebug("Setting scmsync url to ", templateMeta.ScmSync)
}
// Build-disable repositories if asked
if len(buildDisableRepos) > 0 {
toDisable := make([]DisableFlag, len(buildDisableRepos))
for idx, repositoryName := range buildDisableRepos {
toDisable[idx] = DisableFlag{Name: repositoryName}
}
output, err := xml.Marshal(toDisable)
if err != nil {
common.LogError("error while marshalling, skipping BuildDisableRepos: ", err)
} else {
templateMeta.BuildFlags.Contents += string(output)
}
}
}
// Cleanup ReleaseTarget and modify affected path entries
for idx, r := range templateMeta.Repositories {
templateMeta.Repositories[idx].ReleaseTargets = nil
@@ -843,10 +734,13 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
if !stagingConfig.RebuildAll {
for pkg, headOid := range headSubmodules {
if baseOid, exists := baseSubmodules[pkg]; !exists || baseOid != headOid {
if pkg != "rpms" && pkg != "dependencies" {
_, spkg := filepath.Split(pkg)
if exists {
modifiedPackages = append(modifiedPackages, pkg)
modifiedPackages = append(modifiedPackages, spkg)
} else {
newPackages = append(newPackages, pkg)
newPackages = append(newPackages, spkg)
}
}
common.LogDebug(pkg, ":", baseOid, "->", headOid)
}
@@ -941,8 +835,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
CreateQASubProject(stagingConfig, git, gitea, pr,
stagingProject,
setup.Origin,
setup.Name,
setup.BuildDisableRepos)
setup.Name)
msg = msg + ObsWebHost + "/project/show/" +
stagingProject + ":" + setup.Name + "\n"
}
@@ -951,15 +844,11 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
gitea.AddComment(pr, msg)
}
baseResult, err := ObsClient.LastBuildResults(stagingConfig.ObsProject, modifiedPackages...)
if err != nil {
common.LogError("failed fetching ref project status for", stagingConfig.ObsProject, ":", err)
}
stagingResult, err := ObsClient.BuildStatus(stagingProject)
if err != nil {
common.LogError("failed fetching stage project status for", stagingProject, ":", err)
}
buildStatus := ProcessBuildStatus(stagingResult, baseResult)
buildStatus := ProcessBuildStatus(stagingResult)
done := false
switch buildStatus {

View File

@@ -1,15 +0,0 @@
[Unit]
Description=Group Review bot for %i
After=network-online.target
[Service]
Type=exec
ExecStart=/usr/bin/group-review %i
EnvironmentFile=-/etc/default/group-review/%i.env
DynamicUser=yes
NoNewPrivileges=yes
ProtectSystem=strict
[Install]
WantedBy=multi-user.target

View File

@@ -1,41 +1,29 @@
Direct Workflow bot
===================
The project submodule is automatically updated by the direct bot whenever a branch is updated in a package repository.
This bot can coexist with the Workflow PR bot, which is instead triggered by a new package PR.
Target Usage
------------
Devel project, where direct pushes to package git are possible.
Areas of responsibility
-----------------------
1. Keep ProjectGit in sync with packages in the organization
* **On pushes to package**: updates the submodule commit ID to the default branch HEAD (as configured in Gitea).
* **On repository adds**: creates a new submodule (if non-empty).
* **On repository removal**: removes the submodule.
**Note:** If you want to revert a change in a package, you need to do that manually in the project git.
* on pushes to package, updates the submodule commit id
to the default branch HEAD (as configured in Gitea)
* on repository adds, creates a new submodule (if non empty)
* on repository removal, removes the submodule
Configuration
-------------
Uses `workflow.config` for configuration.
Uses `workflow.config` for configuration. Parameters
| Field name | Details | Mandatory | Type | Allowed Values | Default |
| ----- | ----- | ----- | ----- | ----- | ----- |
| *Workflows* | Type of workflow | yes | string | “direct” | |
| *Organization* | The organization that holds all the packages | yes | string | | |
| *Branch* | The designated branch for packages | no | string | | blank (default package branch) |
| *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* |
* _Workflows_: ["direct"] -- direct entry enables direct workflow. **Mandatory**
* _Organization_: organization that holds all the packages. **Mandatory**
* _Branch_: branch updated in repo's, or blank for default package branch
* _GitProjectName_: package in above org, or `org/package#branch` for PrjGit. By default assumes `_ObsPrj` with default branch and in the `Organization`
NOTE: `-rm`, `-removed`, `-deleted` are all removed suffixes used to indicate current branch is a placeholder for previously existing package. These branches will be ignored by the bot, and if default, the package will be removed and will not be added to the project.
Environment Variables
Running
-------
* `GITEA_TOKEN` (required)
@@ -49,3 +37,8 @@ Environment Variables
* `AUTOGITS_REPO_PATH` - default is temporary directory
* `AUTOGITS_IDENTITY_FILE` - in case where we need explicit identify path for ssh specified
Target Usage
------------
Devel project, where direct pushes to package git are possible

View File

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

View File

@@ -1,65 +1,55 @@
Workflow-PR bot
===============
Keeps ProjectGit PRs in-sync with the relative PackageGit PRs.
Keeps ProjectGit PR in-sync with a PackageGit PR
Areas of Responsibility
-----------------------
* Detects a PackageGit PR creation against a package and creates a coresponsing PR against the ProjectGit
* When a PackageGit PR is updated, the corresponding PR against the ProjectGit is updated
* Stores reference to the PackageGit PR in the headers of the ProjectGit PR comments, for later reference
* this allows ProjectGit PR to be merged to seperated later (via another tool, for example)
* Initiates all staging workflows via review requests
Target Usage
------------
Any project (devel, codestream, product, etc.) that accepts PRs.
Any project (devel, etc) that accepts PR
Main Tasks
----------
* **Synchronization**:
* When a **PackageGit PR** is created for a package on a specific project branch, a corresponding PR is automatically generated in **ProjectGit**.
* When a PackageGit PR is updated, the corresponding PR against the ProjectGit is updated.
* A link to the PackageGit PR is stored in the body of the ProjectGit PR comments in the following format:
* `PR: organization/package_name!pull_request_number`
* Example: `PR: pool/curl!4`
* It closes an empty ProjectGit PR (e.g., if a PR was initially created for a single package but later integrated into a larger ProjectGit PR).
* It forwards the Work In Progress (WIP) flag to the ProjectGit PR. If the ProjectGit PR references multiple Package PRs, triggering the WIP flag on the ProjectGit PR side only requires a single WIP package PR.
* **Reviewer Management**:
* It adds required reviewers in the ProjectGit PR.
* It adds required reviewers in the PackageGit PR.
* If new commits are added to a PackageGit PR, reviewers who have already approved it will be re-added.
* **Merge Management**:
* Manages PR merges based on configuration flags (`ManualMergeOnly`, `ManualMergeProject`).
* In general, merge only happens if all mandatory reviews are completed.
* **ManualMergeProject** is stricter than **ManualMergeOnly** and has higher priority.
| Flag | Value | Behavior |
| ----- | ----- | ----- |
| ManualMergeProject | true | Both ProjectGit and PackageGit PRs are merged upon an allowed project maintainer commenting "merge ok” in the ProjectGit PR. |
| ManualMergeOnly | true | Both PackageGit PR and ProjectGit PR are merged upon an allowed package maintainer or project maintainer commenting “merge ok” in the PackageGit PR. |
| ManualMergeOnly and ManualMergeProject | false | Both ProjectGit and PackageGit PRs are merged as soon as all reviews are completed in both PrjGit and PkgGit PRs. |
Config file
-----------
JSON
* _Workflows_: ["pr"] -- pr entry enables pr workflow. **Mandatory**
* _Organization_: organization that holds all the packages **Mandatory**
* _Branch_: branch updated in repo's **Mandatory**
* _GitProjectName_: package in above org, or `org/package#branch` for PrjGit. By default assumes `_ObsPrj` with default branch and in the `Organization`
* _Reviewers_: accounts associated with mandatory reviews for PrjGit. Can trigger additional
review requests for PrjGit or associated PkgGit repos. Only when all reviews are
satisfied, will the PrjGit PR be merged. See Reviewers below.
* _ManualMergeOnly_: (true, false) only merge if "merge ok" comment/review by package or project maintainers or reviewers
* _ManualMergeProject_: (true, false) only merge if "merge ok" by project maintainers or reviewers
* _ReviewRequired_: (true, false) ignores that submitter is a maintainer and require a review from other maintainer IFF available
* _NoProjectGitPR_: (true, false) do not create PrjGit PRs, but still process reviews, etc.
* _Permissions_: permissions and associated accounts/groups. See below.
* _Labels_: (string, string) Labels for PRs. See below.
* Filename: `workflow.config`
* Location: ProjectGit
* Format: non-standard JSON (comments allowed)
| Field name | Details | Mandatory | Type | Allowed Values | Default |
| ----- | ----- | ----- | ----- | ----- | ----- |
| *Workflows* | Type of workflow | yes | string | “pr” | |
| *Organization* | The organization where PackageGit PRs are expected to occur | yes | string | | |
| *Branch* | The designated branch for PackageGit PRs | yes | string | | |
| *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 |
| *ManualMergeProject* | Merges are permitted only upon receiving a "merge ok" comment in the ProjectGit PR from project maintainers. | no | bool | true, false | false |
| *ReviewRequired* | (NOT IMPLEMENTED) 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 |
| *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 | | `[]` |
| *ReviewGroups* | If a group is specified in Reviewers, its members are listed here. | no | array of objects | | `[]` |
| *ReviewGroups > Name* | Name of the group | no | string | | |
| *ReviewGroups > Reviewers* | Members of the group | no | array of strings | | |
| *ReviewGroups > Silent* | Add members for notifications. If true, members are not explicitly requested to review. If one member approves, others are removed. | no | bool | true, false | false |
NOTE: `-rm`, `-removed`, `-deleted` are all removed suffixes used to indicate current branch is a placeholder for previously existing package. These branches will be ignored by the bot, and if default, the package will be removed and will not be added to the project.
example:
[
{
"Workflows": ["pr", "direct"],
"Organization": "autogits",
"GitProjectName": "HiddenPrj",
"Branch": "hidden",
"Reviewers": []
},
...
]
Reviewers
---------
@@ -68,30 +58,21 @@ Reviews is a list of accounts that need to review package and/or project. They h
[~][*|-|+]username
A tilde (`~`) before a prefix signifies an advisory reviewer. Their input is requested, but their review status will not otherwise affect the process.
General prefix of ~ indicates advisory reviewer. They will be requested, but ignored otherwise.
Other prefixes indicate project or package association of the reviewer:
* `*` indicates project *and* package
* `-` indicates project-only reviewer
* `+` indicates package-only reviewer
`+` is implied.
`+` is implied. For example
For example: `[foo, -bar, ~*moo]` results in:
* foo: package reviews
* bar: project reviews
* moo: package and project reviews, but ignored
`[foo, -bar, ~*moo]`
Package Deletion Requests
-------------------------
(NOT YET IMPLEMENTED)
* **Removing a Package:**
To remove a package from a project, submit a ProjectGit Pull Request (PR) that removes the corresponding submodule. The bot will then rename the project branch in the pool by appending "-removed" to its name.
* **Adding a Package Again:**
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.
results in
* foo -> package reviews
* bar -> project reviews
* moo -> package and project reviews, but ignored
Labels
@@ -102,38 +83,27 @@ The following labels are used, when defined in Repo/Org.
| Label Config Entry | Default label | Description
|--------------------|----------------|----------------------------------------
| 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
| ReviewPending | review/Pending | Assigned to PR when reviews are still pending
| ReviewDone | review/Done | Assigned to PR when reviews are complete on this particular PR
| ProjectUpdated | | Assigned if defined and git project got modified
Maintainership
--------------
Filename: \_maintainership.json
Location: ProjectGit
Format: JSON
Fields:
Maintainership information is defined per project. For reviews, package maintainers are coalesced
with project maintainers. A review by any of the maintainers is acceptable.
| Key | Value | Notes |
| ----- | ----- | ----- |
| package name | array of strings representing the package maintainers | List of package maintainers |
| “” (empty string) | array of strings representing the project maintainers | List of project maintainers |
example:
Maintainership information is defined per project. For PackageGit PR reviews, package maintainers are combined with project maintainers. A review by any of these maintainers is acceptable.
{
"package1": [ "reviewer", "reviewer2"],
"package2": [],
If the submitter is a maintainer it will not get a review requested.
// "project" maintainer
"": ["reviewer3", "reviewer4"]
}
Example:
```
{
"package1": [ "reviewer", "reviewer2"],
"package2": [],
// "project" maintainer
"": ["reviewer3", "reviewer4"]
}
```
Permissions
-----------
@@ -153,11 +123,3 @@ the `workflow.config`.
NOTE: Project Maintainers have these permissions automatically.
Server configuration
--------------------------
**Configuration file:**
| Field | Type | Notes |
| ----- | ----- | ----- |
| root | Array of string | Format **org/repo\#branch** |

View File

@@ -170,7 +170,7 @@ func main() {
common.RequestType_PRSync: req,
common.RequestType_PRReviewAccepted: req,
common.RequestType_PRReviewRejected: req,
common.RequestType_PRComment: req,
common.RequestType_IssueComment: req,
},
}
listenDefs.Connection().RabbitURL, _ = url.Parse(*rabbitUrl)

View File

@@ -26,6 +26,7 @@ func PrjGitDescription(prset *common.PRSet) (title string, desc string) {
title_refs := make([]string, 0, len(prset.PRs)-1)
refs := make([]string, 0, len(prset.PRs)-1)
prefix := ""
for _, pr := range prset.PRs {
if prset.IsPrjGitPR(pr.PR) {
continue
@@ -34,6 +35,9 @@ func PrjGitDescription(prset *common.PRSet) (title string, desc string) {
// remove PRs that are not open from description
continue
}
if strings.HasPrefix(pr.PR.Title, "WIP:") {
prefix = "WIP: "
}
org, repo, idx := pr.PRComponents()
title_refs = append(title_refs, repo)
@@ -44,7 +48,7 @@ func PrjGitDescription(prset *common.PRSet) (title string, desc string) {
slices.Sort(title_refs)
slices.Sort(refs)
title = "Forwarded PRs: " + strings.Join(title_refs, ", ")
title = prefix + "Forwarded PRs: " + strings.Join(title_refs, ", ")
desc = fmt.Sprintf("This is a forwarded pull request by %s\nreferencing the following pull request(s):\n\n", GitAuthor) + strings.Join(refs, "\n") + "\n"
if prset.Config.ManualMergeOnly {
@@ -80,6 +84,7 @@ func updateSubmoduleInPR(submodule, headSha string, git common.Git) {
git.GitExec(common.DefaultGitPrj, "submodule", "update", "--init", "--checkout", "--depth", "1", submodule)
common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submodule), "fetch", "--depth", "1", "origin", headSha))
common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submodule), "checkout", "-f", headSha))
common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submodule), "lfs", "fsck"))
}
type PRProcessor struct {
@@ -297,14 +302,16 @@ func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
PrjGit := PrjGitPR.PR.Base.Repo
prjGitPRbranch := PrjGitPR.PR.Head.Name
if PrjGitPR.PR.Base.RepoID != PrjGitPR.PR.Head.RepoID {
PrjGitPR.RemoteName, err = git.GitClone(common.DefaultGitPrj, "", PrjGit.SSHURL)
git.GitExecOrPanic(common.DefaultGitPrj, "fetch", PrjGitPR.RemoteName, PrjGitPR.PR.Head.Sha)
git.GitExecOrPanic(common.DefaultGitPrj, "checkout", PrjGitPR.PR.Head.Sha)
common.LogInfo("Cannot update this PR as it's on another remote, not branch:", prjGitPRbranch, "Assuming this is by-design. (eg. project git PR only)")
return nil
// permission check, if submission comes from foreign repo
if !PrjGitPR.PR.AllowMaintainerEdit {
common.LogError("Warning: source and target branch are in different repositories. We may not have the right permissions...")
// Gitea.AddComment(PrjGitPR.PR, "This PR does not allow maintainer changes, but referenced package branch has changed!")
return nil
}
}
PrjGitPR.RemoteName, err = git.GitClone(common.DefaultGitPrj, prjGitPRbranch, PrjGit.SSHURL)
// PrjGitPR.RemoteName, err = git.GitClone(common.DefaultGitPrj, prjGitPRbranch, PrjGit.SSHURL)
PrjGitPR.RemoteName, err = git.GitClone(common.DefaultGitPrj, PrjGitPR.PR.Head.Ref, PrjGitPR.PR.Head.Repo.SSHURL)
common.PanicOnError(err)
git.GitExecOrPanic(common.DefaultGitPrj, "fetch", PrjGitPR.RemoteName, PrjGitBranch)
@@ -352,6 +359,7 @@ func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
}
common.PanicOnError(git.GitExec(common.DefaultGitPrj, params...))
PrjGitPR.PR.Head.Sha = newHeadCommit
Gitea.SetLabels(PrjGit.Owner.UserName, PrjGit.Name, PrjGitPR.PR.Index, []string{prset.Config.Label("PR/updated")})
}
// update PR
@@ -368,7 +376,7 @@ func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
}
return CurrentTitle == NewTitle
}
if PrjGitPR.PR.User.UserName == CurrentUser.UserName && (PrjGitPR.PR.Body != PrjGitBody || !isPrTitleSame(PrjGitPR.PR.Title, PrjGitTitle)) {
if !pr.config.NoProjectGitPR && PrjGitPR.PR.User.UserName == CurrentUser.UserName && (PrjGitPR.PR.Body != PrjGitBody || !isPrTitleSame(PrjGitPR.PR.Title, PrjGitTitle)) {
Gitea.UpdatePullRequest(PrjGit.Owner.UserName, PrjGit.Name, PrjGitPR.PR.Index, &models.EditPullRequestOption{
RemoveDeadline: true,
Title: PrjGitTitle,
@@ -459,11 +467,11 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
if _, ok := err.(*repository.RepoMergePullRequestConflict); !ok {
common.PanicOnError(err)
}
} else {
Gitea.AddComment(pr.PR, "Closing here because the associated Project PR has been closed.")
Gitea.UpdatePullRequest(org, repo, idx, &models.EditPullRequestOption{
State: "closed",
})
// } else {
// Gitea.AddComment(pr.PR, "Closing here because the associated Project PR has been closed.")
// Gitea.UpdatePullRequest(org, repo, idx, &models.EditPullRequestOption{
// State: "closed",
// })
}
}
}
@@ -476,10 +484,10 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
continue
}
}
}
if err = pr.UpdatePrjGitPR(prset); err != nil {
return err
if err = pr.UpdatePrjGitPR(prset); err != nil {
return err
}
}
}
@@ -554,14 +562,6 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
return err
}
// update prset if we should build it or not
if prjGitPR != nil {
if file, err := git.GitCatFile(common.DefaultGitPrj, prjGitPR.PR.Head.Sha, "staging.config"); err == nil {
prset.HasAutoStaging = (file != nil)
common.LogDebug(" -> automatic staging enabled?:", prset.HasAutoStaging)
}
}
// handle case where PrjGit PR is only one left and there are no changes, then we can just close the PR
if len(prset.PRs) == 1 && prjGitPR != nil && prset.PRs[0] == prjGitPR && prjGitPR.PR.User.UserName == prset.BotUser {
common.LogDebug(" --> checking if superflous PR")