Hello community,
here is the log from the commit of package openSUSE-release-tools for
openSUSE:Factory checked in at 2018-08-31 10:41:43
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/openSUSE-release-tools (Old)
and /work/SRC/openSUSE:Factory/.openSUSE-release-tools.new (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "openSUSE-release-tools"
Fri Aug 31 10:41:43 2018 rev:123 rq:630991 version:20180822.a9f1bc0
Changes:
--------
---
/work/SRC/openSUSE:Factory/openSUSE-release-tools/openSUSE-release-tools.changes
2018-08-22 14:20:03.950310868 +0200
+++
/work/SRC/openSUSE:Factory/.openSUSE-release-tools.new/openSUSE-release-tools.changes
2018-08-31 10:43:22.283125874 +0200
@@ -1,0 +2,23 @@
+Wed Aug 22 22:38:19 UTC 2018 - [email protected]
+
+- Update to version 20180822.a9f1bc0:
+ * osclib/core: repository_path_expand(): skip adding duplicate path.
+
+-------------------------------------------------------------------
+Wed Aug 22 02:02:18 UTC 2018 - [email protected]
+
+- Update to version 20180821.fa39e68:
+ * StagingAPI: drop inferior expanded_repos() implementation for osclib.core.
+ * pkglistgen: utilize osclib.core.repository_path_expand().
+ * repo_checker: complete rework to handle arbitrary repos and maintenance.
+ * osclib/util: provide sha1_short() adapted from repo_checker.
+ * osclib/core: provide project_meta_revision() adapted from repo_checker.
+ * osclib/core: provide repository state and published functions.
+ * osclib/core: provide repository_path_search().
+ * osclib/core: provide repository_path_expand() adapted from StagingAPI.
+ * osclib/core: target_archs(): expose repository argument.
+ * osclib/conf: drop main-repo default for all projects.
+ * ReviewBot: utilize osclib.Cache for all bots by default.
+ * ReviewBot: utilize memoize cached config.
+
+-------------------------------------------------------------------
Old:
----
openSUSE-release-tools-20180820.d7d5724.obscpio
New:
----
openSUSE-release-tools-20180822.a9f1bc0.obscpio
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ openSUSE-release-tools.spec ++++++
--- /var/tmp/diff_new_pack.nozftN/_old 2018-08-31 10:43:23.111126855 +0200
+++ /var/tmp/diff_new_pack.nozftN/_new 2018-08-31 10:43:23.115126860 +0200
@@ -20,7 +20,7 @@
%define source_dir openSUSE-release-tools
%define announcer_filename factory-package-news
Name: openSUSE-release-tools
-Version: 20180820.d7d5724
+Version: 20180822.a9f1bc0
Release: 0
Summary: Tools to aid in staging and release work for openSUSE/SUSE
License: GPL-2.0-or-later AND MIT
++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.nozftN/_old 2018-08-31 10:43:23.143126893 +0200
+++ /var/tmp/diff_new_pack.nozftN/_new 2018-08-31 10:43:23.143126893 +0200
@@ -1,6 +1,6 @@
<servicedata>
<service name="tar_scm">
<param
name="url">https://github.com/openSUSE/openSUSE-release-tools.git</param>
- <param
name="changesrevision">1fe03c16f0b836db70d0bd4145161a1199952a60</param>
+ <param
name="changesrevision">a8cfd74f1f7a4a1a38fd2530262d0b7dd06d8177</param>
</service>
</servicedata>
++++++ openSUSE-release-tools-20180820.d7d5724.obscpio ->
openSUSE-release-tools-20180822.a9f1bc0.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/openSUSE-release-tools-20180820.d7d5724/ReviewBot.py
new/openSUSE-release-tools-20180822.a9f1bc0/ReviewBot.py
--- old/openSUSE-release-tools-20180820.d7d5724/ReviewBot.py 2018-08-21
04:03:44.000000000 +0200
+++ new/openSUSE-release-tools-20180822.a9f1bc0/ReviewBot.py 2018-08-23
00:33:11.000000000 +0200
@@ -26,6 +26,7 @@
import cmdln
from collections import namedtuple
from collections import OrderedDict
+from osclib.cache import Cache
from osclib.comments import CommentAPI
from osclib.conf import Config
from osclib.core import group_members
@@ -141,7 +142,7 @@
def staging_api(self, project):
if project not in self.staging_apis:
- Config(self.apiurl, project)
+ Config.get(self.apiurl, project)
self.staging_apis[project] = StagingAPI(self.apiurl, project)
return self.staging_apis[project]
@@ -680,6 +681,7 @@
class CommandLineInterface(cmdln.Cmdln):
def __init__(self, *args, **kwargs):
cmdln.Cmdln.__init__(self, args, kwargs)
+ Cache.init()
self.clazz = ReviewBot
def get_optparser(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20180820.d7d5724/osclib/conf.py
new/openSUSE-release-tools-20180822.a9f1bc0/osclib/conf.py
--- old/openSUSE-release-tools-20180820.d7d5724/osclib/conf.py 2018-08-21
04:03:44.000000000 +0200
+++ new/openSUSE-release-tools-20180822.a9f1bc0/osclib/conf.py 2018-08-23
00:33:11.000000000 +0200
@@ -58,6 +58,7 @@
'review-team': 'opensuse-review-team',
'legal-review-group': 'legal-auto',
'repo-checker': 'repo-checker',
+ 'repo_checker-no-filter': 'True',
'pkglistgen-product-family-include': 'openSUSE:Leap:N',
'mail-list': '[email protected]',
'mail-maintainer': 'Dominique Leuenberger <[email protected]>',
@@ -91,6 +92,7 @@
# review-team optionally added by leaper.py.
'repo-checker': 'repo-checker',
'repo_checker-arch-whitelist': 'x86_64',
+ 'repo_checker-no-filter': 'True',
# 16 hour staging window for follow-ups since lower throughput.
'splitter-staging-age-max': '57600',
# No special packages since they will pass through SLE first.
@@ -115,6 +117,7 @@
'main-repo': 'standard',
'leaper-override-group': 'leap-reviewers',
'repo_checker-arch-whitelist': 'x86_64',
+ 'repo_checker-no-filter': 'True',
},
r'openSUSE:(?P<project>Backports:(?P<version>[^:]+))$': {
'staging': 'openSUSE:%(project)s:Staging',
@@ -153,7 +156,6 @@
'lock': None,
'lock-ns': None,
'delreq-review': None,
- 'main-repo': 'openSUSE_Factory',
'_priority': '0', # Apply defaults first
},
}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20180820.d7d5724/osclib/core.py
new/openSUSE-release-tools-20180822.a9f1bc0/osclib/core.py
--- old/openSUSE-release-tools-20180820.d7d5724/osclib/core.py 2018-08-21
04:03:44.000000000 +0200
+++ new/openSUSE-release-tools-20180822.a9f1bc0/osclib/core.py 2018-08-23
00:33:11.000000000 +0200
@@ -13,6 +13,7 @@
from urllib2 import HTTPError
from osc.core import get_binarylist
+from osc.core import get_commitlog
from osc.core import get_dependson
from osc.core import http_GET
from osc.core import http_POST
@@ -22,6 +23,7 @@
from osc.core import Request
from osc.core import show_package_meta
from osc.core import show_project_meta
+from osc.core import show_results_meta
from osclib.conf import Config
from osclib.memoize import memoize
@@ -89,18 +91,14 @@
return sorted(packages)
@memoize(session=True)
-def target_archs(apiurl, project):
- meta = show_project_meta(apiurl, project)
- meta = ET.fromstringlist(meta)
- archs = []
- for arch in meta.findall('repository[@name="standard"]/arch'):
- archs.append(arch.text)
- return archs
+def target_archs(apiurl, project, repository='standard'):
+ meta = ETL.fromstringlist(show_project_meta(apiurl, project))
+ return meta.xpath('repository[@name="{}"]/arch/text()'.format(repository))
@memoize(session=True)
def depends_on(apiurl, project, repository, packages=None, reverse=None):
dependencies = set()
- for arch in target_archs(apiurl, project):
+ for arch in target_archs(apiurl, project, repository):
root = ET.fromstring(get_dependson(apiurl, project, repository, arch,
packages, reverse))
dependencies.update(pkgdep.text for pkgdep in
root.findall('.//pkgdep'))
@@ -114,18 +112,6 @@
return date_parse(when)
-def request_staged(request):
- for review in request.reviews:
- if (review.state == 'new' and review.by_project and
- review.by_project.startswith(request.actions[0].tgt_project)):
-
- # Allow time for things to settle.
- when = request_when_staged(request, review.by_project)
- if (datetime.utcnow() - when).total_seconds() > 10 * 60:
- return review.by_project
-
- return None
-
def binary_list(apiurl, project, repository, arch, package=None):
parsed = []
for binary in get_binarylist(apiurl, project, repository, arch, package):
@@ -349,3 +335,80 @@
# The OBS API of attributes is super strange, POST to update.
url = makeurl(apiurl, ['source', project, '_attribute'])
http_POST(url, data=ET.tostring(root))
+
+@memoize(session=True)
+def repository_path_expand(apiurl, project, repo, repos=None):
+ """Recursively list underlying projects."""
+
+ if repos is None:
+ # Avoids screwy behavior where list as default shares reference for all
+ # calls which effectively means the list grows even when new project.
+ repos = []
+
+ if [project, repo] in repos:
+ # For some reason devel projects such as graphics include the same path
+ # twice for openSUSE:Factory/snapshot. Does not hurt anything, but
+ # cleaner not to include it twice.
+ return repos
+
+ repos.append([project, repo])
+
+ meta = ET.fromstringlist(show_project_meta(apiurl, project))
+ for path in meta.findall('.//repository[@name="{}"]/path'.format(repo)):
+ repository_path_expand(apiurl, path.get('project', project),
path.get('repository'), repos)
+
+ return repos
+
+@memoize(session=True)
+def repository_path_search(apiurl, project, search_project, search_repository):
+ queue = []
+
+ # Initialize breadth first search queue with repositories from top project.
+ root = ETL.fromstringlist(show_project_meta(apiurl, project))
+ for repository in root.xpath('repository[path[@project and
@repository]]/@name'):
+ queue.append((repository, project, repository))
+
+ # Perform a breadth first search and return the first repository chain with
+ # a series of path elements targeting search project and repository.
+ for repository_top, project, repository in queue:
+ if root.get('name') != project:
+ # Repositories for a single project are in a row so cache parsing.
+ root = ETL.fromstringlist(show_project_meta(apiurl, project))
+
+ paths = root.findall('repository[@name="{}"]/path'.format(repository))
+ for path in paths:
+ if path.get('project') == search_project and
path.get('repository') == search_repository:
+ return repository_top
+
+ queue.append((repository_top, path.get('project'),
path.get('repository')))
+
+ return None
+
+def repository_state(apiurl, project, repository):
+ return ET.fromstringlist(show_results_meta(
+ apiurl, project, multibuild=True,
repository=[repository])).get('state')
+
+def repositories_states(apiurl, repository_pairs):
+ states = []
+
+ for project, repository in repository_pairs:
+ states.append(repository_state(apiurl, project, repository))
+
+ return states
+
+def repository_published(apiurl, project, repository):
+ root = ETL.fromstringlist(show_results_meta(
+ apiurl, project, multibuild=True, repository=[repository]))
+ return not len(root.xpath('result[@state!="published" and
@state!="unpublished"]'))
+
+def repositories_published(apiurl, repository_pairs):
+ for project, repository in repository_pairs:
+ if not repository_published(apiurl, project, repository):
+ return (project, repository)
+
+ return True
+
+def project_meta_revision(apiurl, project):
+ root = ET.fromstringlist(get_commitlog(
+ apiurl, project, '_project', None, format='xml', meta=True))
+ return int(root.find('logentry').get('revision'))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20180820.d7d5724/osclib/cycle.py
new/openSUSE-release-tools-20180822.a9f1bc0/osclib/cycle.py
--- old/openSUSE-release-tools-20180820.d7d5724/osclib/cycle.py 2018-08-21
04:03:44.000000000 +0200
+++ new/openSUSE-release-tools-20180822.a9f1bc0/osclib/cycle.py 2018-08-23
00:33:11.000000000 +0200
@@ -202,15 +202,12 @@
graph.subpkgs = subpkgs
return graph
- def cycles(self, staging, project=None, repository='standard',
arch='x86_64'):
+ def cycles(self, override_pair, overridden_pair, arch):
"""Detect cycles in a specific repository."""
- if not project:
- project = self.api.project
-
# Detect cycles - We create the full graph from _builddepinfo.
- project_graph = self._get_builddepinfo_graph(project, repository, arch)
- current_graph = self._get_builddepinfo_graph(staging, repository, arch)
+ project_graph = self._get_builddepinfo_graph(overridden_pair[0],
overridden_pair[1], arch)
+ current_graph = self._get_builddepinfo_graph(override_pair[0],
override_pair[1], arch)
# Sometimes, new cycles have only new edges, but not new
# packages. We need to inform about this, so this can become
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20180820.d7d5724/osclib/stagingapi.py
new/openSUSE-release-tools-20180822.a9f1bc0/osclib/stagingapi.py
--- old/openSUSE-release-tools-20180820.d7d5724/osclib/stagingapi.py
2018-08-21 04:03:44.000000000 +0200
+++ new/openSUSE-release-tools-20180822.a9f1bc0/osclib/stagingapi.py
2018-08-23 00:33:11.000000000 +0200
@@ -1792,15 +1792,3 @@
return meta.find(xpath) is not None
return False
-
- # recursively detect underlying projects
- def expand_project_repo(self, project, repo, repos):
- repos.append([project, repo])
- url = self.makeurl(['source', project, '_meta'])
- meta = ET.parse(self.retried_GET(url)).getroot()
- for path in
meta.findall('.//repository[@name="{}"]/path'.format(repo)):
- self.expand_project_repo(path.get('project', project),
path.get('repository'), repos)
- return repos
-
- def expanded_repos(self, repo):
- return self.expand_project_repo(self.project, repo, [])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20180820.d7d5724/osclib/util.py
new/openSUSE-release-tools-20180822.a9f1bc0/osclib/util.py
--- old/openSUSE-release-tools-20180820.d7d5724/osclib/util.py 2018-08-21
04:03:44.000000000 +0200
+++ new/openSUSE-release-tools-20180822.a9f1bc0/osclib/util.py 2018-08-23
00:33:11.000000000 +0200
@@ -104,3 +104,11 @@
s = smtplib.SMTP(config.get('mail-relay', 'relay.suse.de'))
s.sendmail(msg['From'], [msg['To']], msg.as_string())
s.quit()
+
+def sha1_short(data):
+ import hashlib
+
+ if isinstance(data, list):
+ data = '::'.join(data)
+
+ return hashlib.sha1(data).hexdigest()[:7]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20180820.d7d5724/pkglistgen.py
new/openSUSE-release-tools-20180822.a9f1bc0/pkglistgen.py
--- old/openSUSE-release-tools-20180820.d7d5724/pkglistgen.py 2018-08-21
04:03:44.000000000 +0200
+++ new/openSUSE-release-tools-20180822.a9f1bc0/pkglistgen.py 2018-08-23
00:33:11.000000000 +0200
@@ -40,6 +40,7 @@
from osc.core import undelete_package
from osc import conf
from osclib.conf import Config, str2bool
+from osclib.core import repository_path_expand
from osclib.stagingapi import StagingAPI
from osclib.util import project_list_family
from osclib.util import project_list_family_prior
@@ -525,7 +526,7 @@
g.ignore(self.groups[e])
def expand_repos(self, project, repo='standard'):
- return StagingAPI(self.apiurl, project).expanded_repos(repo)
+ return repository_path_expand(self.apiurl, project, repo)
def _check_supplements(self):
tocheck = set()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20180820.d7d5724/repo_checker.py
new/openSUSE-release-tools-20180822.a9f1bc0/repo_checker.py
--- old/openSUSE-release-tools-20180820.d7d5724/repo_checker.py 2018-08-21
04:03:44.000000000 +0200
+++ new/openSUSE-release-tools-20180822.a9f1bc0/repo_checker.py 2018-08-23
00:33:11.000000000 +0200
@@ -14,21 +14,26 @@
import sys
import tempfile
-from osclib.comments import CommentAPI
from osclib.conf import Config
from osclib.conf import str2bool
-from osclib.core import binary_list
from osclib.core import BINARY_REGEX
from osclib.core import depends_on
from osclib.core import devel_project_fallback
from osclib.core import fileinfo_ext_all
from osclib.core import package_binary_list
+from osclib.core import project_meta_revision
+from osclib.core import project_pseudometa_file_ensure
+from osclib.core import project_pseudometa_file_load
from osclib.core import project_pseudometa_package
-from osclib.core import request_staged
+from osclib.core import repository_path_search
+from osclib.core import repository_path_expand
+from osclib.core import repositories_states
+from osclib.core import repositories_published
from osclib.core import target_archs
from osclib.cycle import CycleDetector
from osclib.memoize import CACHEDIR
from osclib.memoize import memoize
+from osclib.util import sha1_short
import ReviewBot
@@ -37,67 +42,29 @@
INSTALL_REGEX = r"^(?:can't install (.*?)|found conflict of (.*?) with
(.*?)):$"
InstallSection = namedtuple('InstallSection', ('binaries', 'text'))
+ERROR_REPO_SPECIFIED = 'a repository must be specified via OSRT:Config
main-repo for {}'
+
class RepoChecker(ReviewBot.ReviewBot):
def __init__(self, *args, **kwargs):
ReviewBot.ReviewBot.__init__(self, *args, **kwargs)
# ReviewBot options.
- self.only_one_action = True
self.request_default_return = True
self.comment_handler = True
# RepoChecker options.
self.skip_cycle = False
self.force = False
- self.limit_group = None
-
- def repository_published(self, project):
- root = ET.fromstringlist(show_results_meta(
- self.apiurl, project, multibuild=True, repository=['standard']))
- return not len(root.xpath('result[@state!="published"]'))
def project_only(self, project, post_comments=False):
- api = self.staging_api(project)
-
- if not self.force and not self.repository_published(project):
- self.logger.info('{}/standard not published'.format(project))
+ repository = self.project_repository(project)
+ if not repository:
+ self.logger.error(ERROR_REPO_SPECIFIED.format(project))
return
- build = ET.fromstringlist(show_results_meta(
- self.apiurl, project, multibuild=True,
repository=['standard'])).get('state')
- pseudometa_content = api.pseudometa_file_load('repo_checker')
- if not self.force and pseudometa_content:
- build_previous = pseudometa_content.splitlines()[0]
- if build == build_previous:
- self.logger.info('{} build unchanged'.format(project))
- return
-
- comment = [build]
- for arch in self.target_archs(project):
- directories = []
- repos = self.staging_api(project).expanded_repos('standard')
- for layered_project, repo in repos:
- if repo != 'standard':
- raise "We assume all is standard"
- directories.append(self.mirror(layered_project, arch))
-
- parse = project if post_comments else False
- results = {
- 'cycle': CheckResult(True, None),
- 'install': self.install_check(project, directories, arch,
parse=parse, no_filter=True),
- }
-
- if not all(result.success for _, result in results.items()):
- self.result_comment(arch, results, comment)
-
- text = '\n'.join(comment).strip()
- if not self.dryrun:
- api.pseudometa_file_ensure('repo_checker', text + '\n',
'project_only run')
- else:
- print(text)
-
- if post_comments:
- self.package_comments(project)
+ repository_pairs = repository_path_expand(self.apiurl, project,
repository)
+ state_hash = self.repository_state(repository_pairs)
+ self.repository_check(repository_pairs, state_hash, False)
def package_comments(self, project):
self.logger.info('{} package
comments'.format(len(self.package_results)))
@@ -137,192 +104,26 @@
self.comment_write(state='seen', result=reference,
bot_name_suffix=bot_name_suffix,
project=comment_project,
package=comment_package, message=message)
- def prepare_review(self):
- # Reset for request batch.
- self.requests_map = {}
- self.groups = {}
- self.groups_build = {}
- self.groups_skip_cycle = []
-
- # Manipulated in ensure_group().
- self.group = None
- self.mirrored = set()
-
- # Stores parsed install_check() results grouped by package.
- self.package_results = {}
-
- # Look for requests of interest and group by staging.
- skip_build = set()
- for request in self.requests:
- # Only interesting if request is staged.
- group = request_staged(request)
- if not group:
- self.logger.debug('{}: not staged'.format(request.reqid))
- continue
-
- if self.limit_group and group != self.limit_group:
- continue
-
- # Only interested if group has completed building.
- api = self.staging_api(request.actions[0].tgt_project)
- status = api.project_status(group, True)
- # Corrupted requests may reference non-existent projects and will
- # thus return a None status which should be considered not ready.
- if not status or str(status['overall_state']) not in ('testing',
'review', 'acceptable'):
- # Not in a "ready" state.
- openQA_only = False # Not relevant so set to False.
- if status and str(status['overall_state']) == 'failed':
- # Exception to the rule is openQA only in failed state,
- # Broken packages so not just openQA.
- openQA_only = (len(status['broken_packages']) == 0)
-
- if not self.force and not openQA_only:
- self.logger.debug('{}: {} not ready'.format(request.reqid,
group))
- continue
-
- # Only interested if request is in consistent state.
- selected = api.project_status_requests('selected')
- if request.reqid not in selected:
- self.logger.debug('{}: inconsistent
state'.format(request.reqid))
-
- if group not in self.groups_build:
- # Generate build hash based on hashes from relevant projects.
- builds = []
- builds.append(ET.fromstringlist(show_results_meta(
- self.apiurl, group, multibuild=True,
repository=['standard'])).get('state'))
- builds.append(ET.fromstringlist(show_results_meta(
- self.apiurl, api.project, multibuild=True,
repository=['standard'])).get('state'))
-
- # Include meta revision for config changes (like whitelist).
- builds.append(str(api.get_prj_meta_revision(group)))
- self.groups_build[group] =
hashlib.sha1(''.join(builds)).hexdigest()[:7]
-
- # Determine if build has changed since last comment.
- comments = self.comment_api.get_comments(project_name=group)
- _, info = self.comment_api.comment_find(comments,
self.bot_name)
- if info and self.groups_build[group] == info.get('build'):
- skip_build.add(group)
-
- # Look for skip-cycle comment command.
- users =
self.request_override_check_users(request.actions[0].tgt_project)
- for _, who in self.comment_api.command_find(
- comments, self.review_user, 'skip-cycle', users):
- self.logger.debug('comment command: skip-cycle by
{}'.format(who))
- self.groups_skip_cycle.append(group)
- break
-
- if not self.force and group in skip_build:
- self.logger.debug('{}: {} build
unchanged'.format(request.reqid, group))
- continue
-
- self.requests_map[int(request.reqid)] = group
-
- requests = self.groups.get(group, [])
- requests.append(request)
- self.groups[group] = requests
-
- self.logger.debug('{}: {} ready'.format(request.reqid, group))
-
- # Filter out undesirable requests and ensure requests are ordered
- # together with group for efficiency.
- count_before = len(self.requests)
- self.requests = []
- for group, requests in sorted(self.groups.items()):
- self.requests.extend(requests)
-
- self.logger.debug('requests: {} skipped, {} queued'.format(
- count_before - len(self.requests), len(self.requests)))
-
- def ensure_group(self, request, action):
- project = action.tgt_project
- group = self.requests_map[int(request.reqid)]
-
- if group == self.group:
- # Only process a group the first time it is encountered.
- return self.group_pass
-
- self.logger.info('group {}'.format(group))
- self.group = group
- self.group_pass = True
-
- comment = []
- for arch in self.target_archs(project):
- stagings = []
- directories = []
- ignore = set()
-
- if arch not in self.target_archs(group):
- self.logger.debug('{}/{} not available'.format(group, arch))
- else:
- stagings.append(group)
- directories.append(self.mirror(group, arch))
- ignore.update(self.ignore_from_staging(project, group, arch))
-
- if not len(stagings):
- continue
-
- # Only bother if staging can match arch, but layered first.
- repos = self.staging_api(project).expanded_repos('standard')
- for layered_project, repo in repos:
- if repo != 'standard':
- raise "We assume all is standard"
- directories.append(self.mirror(layered_project, arch))
-
- whitelist = self.binary_whitelist(project, arch, group)
-
- # Perform checks on group.
- results = {
- 'cycle': self.cycle_check(project, stagings, arch),
- 'install': self.install_check(project, directories, arch,
ignore, whitelist),
- }
-
- if not all(result.success for _, result in results.items()):
- # Not all checks passed, build comment.
- self.group_pass = False
- self.result_comment(arch, results, comment)
-
- info_extra = {'build': self.groups_build[group]}
- if not self.group_pass:
- # Some checks in group did not pass, post comment.
- # Avoid identical comments with different build hash during target
- # project build phase. Once published update regardless.
- published = self.repository_published(project)
- self.comment_write(state='seen', result='failed', project=group,
- message='\n'.join(comment).strip(),
identical=True,
- info_extra=info_extra,
info_extra_identical=published)
- else:
- # Post passed comment only if previous failed comment.
- text = 'Previously reported problems have been resolved.'
- self.comment_write(state='done', result='passed', project=group,
- message=text, identical=True, only_replace=True,
- info_extra=info_extra)
-
- return self.group_pass
-
- def target_archs(self, project):
- archs = target_archs(self.apiurl, project)
+ def target_archs(self, project, repository):
+ archs = target_archs(self.apiurl, project, repository)
# Check for arch whitelist and use intersection.
- product = project.split(':Staging:', 1)[0]
- whitelist = Config.get(self.apiurl,
product).get('repo_checker-arch-whitelist')
+ whitelist = Config.get(self.apiurl,
project).get('repo_checker-arch-whitelist')
if whitelist:
archs = list(set(whitelist.split(' ')).intersection(set(archs)))
# Trick to prioritize x86_64.
return sorted(archs, reverse=True)
- def mirror(self, project, arch):
+ @memoize(ttl=60, session=True, add_invalidate=True)
+ def mirror(self, project, repository, arch):
"""Call bs_mirrorfull script to mirror packages."""
- directory = os.path.join(CACHEDIR, project, 'standard', arch)
- if (project, arch) in self.mirrored:
- # Only mirror once per request batch.
- return directory
-
+ directory = os.path.join(CACHEDIR, project, repository, arch)
if not os.path.exists(directory):
os.makedirs(directory)
script = os.path.join(SCRIPT_PATH, 'bs_mirrorfull')
- path = '/'.join((project, 'standard', arch))
+ path = '/'.join((project, repository, arch))
url = '{}/public/build/{}'.format(self.apiurl, path)
parts = ['LC_ALL=C', 'perl', script, '--nodebug', url, directory]
parts = [pipes.quote(part) for part in parts]
@@ -331,26 +132,25 @@
if os.system(' '.join(parts)):
raise Exception('failed to mirror {}'.format(path))
- self.mirrored.add((project, arch))
return directory
- def ignore_from_staging(self, project, staging, arch):
- """Determine the target project binaries to ingore in favor of
staging."""
- _, binary_map = package_binary_list(self.apiurl, staging, 'standard',
arch)
+ def simulated_merge_ignore(self, override_pair, overridden_pair, arch):
+ """Determine the list of binaries to similate overides in overridden
layer."""
+ _, binary_map = package_binary_list(self.apiurl, override_pair[0],
override_pair[1], arch)
packages = set(binary_map.values())
- binaries, _ = package_binary_list(self.apiurl, project, 'standard',
arch)
+ binaries, _ = package_binary_list(self.apiurl, overridden_pair[0],
overridden_pair[1], arch)
for binary in binaries:
if binary.package in packages:
yield binary.name
@memoize(session=True)
- def binary_list_existing_problem(self, project):
+ def binary_list_existing_problem(self, project, repository):
"""Determine which binaries are mentioned in repo_checker output."""
binaries = set()
- api = self.staging_api(project)
- content = api.pseudometa_file_load('repo_checker')
+ filename = self.project_pseudometa_file_name(project, repository)
+ content = project_pseudometa_file_load(self.apiurl, project, filename)
if not content:
self.logger.warn('no project_only run from which to extract
existing problems')
return binaries
@@ -364,23 +164,29 @@
return binaries
- def binary_whitelist(self, project, arch, group):
- additions =
self.staging_api(project).get_prj_pseudometa(group).get('config', {})
- whitelist = self.binary_list_existing_problem(project)
- prefix = 'repo_checker-binary-whitelist'
- for key in [prefix, '-'.join([prefix, arch])]:
- whitelist.update(additions.get(key, '').split(' '))
+ def binary_whitelist(self, override_pair, overridden_pair, arch):
+ whitelist = self.binary_list_existing_problem(overridden_pair[0],
overridden_pair[1])
+
+ if Config.get(self.apiurl, overridden_pair[0]).get('staging'):
+ additions =
self.staging_api(overridden_pair[0]).get_prj_pseudometa(
+ override_pair[0]).get('config', {})
+ prefix = 'repo_checker-binary-whitelist'
+ for key in [prefix, '-'.join([prefix, arch])]:
+ whitelist.update(additions.get(key, '').split(' '))
+
whitelist = filter(None, whitelist)
return whitelist
- def install_check(self, project, directories, arch, ignore=[],
whitelist=[], parse=False, no_filter=False):
- self.logger.info('install check: start')
+ def install_check(self, project, directories, repository, arch,
ignore=None, whitelist=[], parse=False, no_filter=False):
+ self.logger.info('install check: start (ignore:{}, whitelist:{},
parse:{}, no_filter:{})'.format(
+ bool(ignore), len(whitelist), bool(parse), no_filter))
with tempfile.NamedTemporaryFile() as ignore_file:
# Print ignored rpms on separate lines in ignore file.
- for item in ignore:
- ignore_file.write(item + '\n')
- ignore_file.flush()
+ if ignore:
+ for item in ignore:
+ ignore_file.write(item + '\n')
+ ignore_file.flush()
# Invoke repo_checker.pl to perform an install check.
script = os.path.join(SCRIPT_PATH, 'repo_checker.pl')
@@ -399,11 +205,11 @@
self.logger.info('install check: failed')
if p.returncode == 126:
self.logger.warn('mirror cache reset due to corruption')
- self.mirrored = set()
+ self._invalidate_all()
elif parse:
# Parse output for later consumption for posting comments.
sections = self.install_check_parse(stdout)
- self.install_check_sections_group(parse, arch, sections)
+ self.install_check_sections_group(parse, repository, arch,
sections)
# Format output as markdown comment.
parts = []
@@ -416,16 +222,16 @@
parts.append('<pre>\n' + stderr + '\n' + '</pre>\n')
pseudometa_project, pseudometa_package =
project_pseudometa_package(self.apiurl, project)
- path = ['package', 'view_file', pseudometa_project,
pseudometa_package, 'repo_checker']
+ filename = self.project_pseudometa_file_name(project, repository)
+ path = ['package', 'view_file', pseudometa_project,
pseudometa_package, filename]
header = '### [install check & file
conflicts](/{})\n\n'.format('/'.join(path))
return CheckResult(False, header + ('\n' + ('-' * 80) +
'\n\n').join(parts))
-
self.logger.info('install check: passed')
return CheckResult(True, None)
- def install_check_sections_group(self, project, arch, sections):
- _, binary_map = package_binary_list(self.apiurl, project, 'standard',
arch)
+ def install_check_sections_group(self, project, repository, arch,
sections):
+ _, binary_map = package_binary_list(self.apiurl, project, repository,
arch)
for section in sections:
# If switch to creating bugs likely makes sense to join packages to
@@ -463,36 +269,52 @@
if section:
yield InstallSection(section, text)
- def cycle_check(self, project, stagings, arch):
- if self.skip_cycle or self.group in self.groups_skip_cycle:
+ @memoize(ttl=60, session=True)
+ def cycle_check_skip(self, project):
+ if self.skip_cycle:
+ return True
+
+ # Look for skip-cycle comment command.
+ comments = self.comment_api.get_comments(project_name=project)
+ users = self.request_override_check_users(project)
+ for _, who in self.comment_api.command_find(
+ comments, self.review_user, 'skip-cycle', users):
+ self.logger.debug('comment command: skip-cycle by {}'.format(who))
+ return True
+
+ return False
+
+ def cycle_check(self, override_pair, overridden_pair, arch):
+ if self.cycle_check_skip(override_pair[0]):
self.logger.info('cycle check: skip due to --skip-cycle or comment
command')
return CheckResult(True, None)
self.logger.info('cycle check: start')
- cycle_detector = CycleDetector(self.staging_api(project))
comment = []
- for staging in stagings:
- first = True
- for index, (cycle, new_edges, new_packages) in enumerate(
- cycle_detector.cycles(staging, arch=arch), start=1):
- if not new_packages:
- continue
+ first = True
+ cycle_detector = CycleDetector(self.staging_api(overridden_pair[0]))
+ for index, (cycle, new_edges, new_packages) in enumerate(
+ cycle_detector.cycles(override_pair, overridden_pair, arch),
start=1):
+
+ if not new_packages:
+ continue
- if first:
- comment.append('### new
[cycle(s)](/project/repository_state/{}/standard)\n'.format(staging))
- first = False
-
- # New package involved in cycle, build comment.
- comment.append('- #{}: {} package cycle, {} new edges'.format(
- index, len(cycle), len(new_edges)))
-
- comment.append(' - cycle')
- for package in sorted(cycle):
- comment.append(' - {}'.format(package))
-
- comment.append(' - new edges')
- for edge in sorted(new_edges):
- comment.append(' - ({}, {})'.format(edge[0], edge[1]))
+ if first:
+ comment.append('### new
[cycle(s)](/project/repository_state/{}/{})\n'.format(
+ override_pair[0], override_pair[1]))
+ first = False
+
+ # New package involved in cycle, build comment.
+ comment.append('- #{}: {} package cycle, {} new edges'.format(
+ index, len(cycle), len(new_edges)))
+
+ comment.append(' - cycle')
+ for package in sorted(cycle):
+ comment.append(' - {}'.format(package))
+
+ comment.append(' - new edges')
+ for edge in sorted(new_edges):
+ comment.append(' - ({}, {})'.format(edge[0], edge[1]))
if len(comment):
# New cycles, post comment.
@@ -502,15 +324,210 @@
self.logger.info('cycle check: passed')
return CheckResult(True, None)
- def result_comment(self, arch, results, comment):
+ def result_comment(self, repository, arch, results, comment):
"""Generate comment from results"""
- comment.append('## ' + arch + '\n')
+ comment.append('## {}/{}\n'.format(repository, arch))
for result in results.values():
if not result.success:
comment.append(result.comment)
+ def project_pseudometa_file_name(self, project, repository):
+ filename = 'repo_checker'
+
+ main_repo = Config.get(self.apiurl, project).get('main-repo')
+ if not main_repo:
+ filename += '.' + repository
+
+ return filename
+
+ @memoize(ttl=60, session=True)
+ def repository_state(self, repository_pairs):
+ states = repositories_states(self.apiurl, repository_pairs)
+ states.append(str(project_meta_revision(self.apiurl,
repository_pairs[0][0])))
+
+ return sha1_short(states)
+
+ @memoize(ttl=60, session=True)
+ def repository_state_last(self, project, repository, pseudometa):
+ if pseudometa:
+ filename = self.project_pseudometa_file_name(project, repository)
+ content = project_pseudometa_file_load(self.apiurl, project,
filename)
+ if content:
+ return content.splitlines()[0]
+ else:
+ comments = self.comment_api.get_comments(project_name=project)
+ _, info = self.comment_api.comment_find(comments, self.bot_name)
+ if info:
+ return info.get('build')
+
+ return None
+
+ @memoize(session=True)
+ def repository_check(self, repository_pairs, state_hash, simulate_merge,
post_comments=False):
+ comment = []
+ project, repository = repository_pairs[0] # this would mean staging!?
+ self.logger.info('checking {}/{}@{}[{}]'.format(
+ project, repository, state_hash, len(repository_pairs)))
+
+ published = repositories_published(self.apiurl, repository_pairs)
+
+ if not self.force:
+ if state_hash == self.repository_state_last(project, repository,
not simulate_merge):
+ self.logger.info('{} build unchanged'.format(project))
+ # TODO keep track of skipped count for cycle summary
+ return None
+
+ # For submit style requests, want to process if top layer is done,
+ # but not mark review as final until all layers are published.
+ if published is not True and (not simulate_merge or published[0]
== project):
+ # Require all layers to be published except when the top layer
+ # is published in a simulate merge (allows quicker feedback
with
+ # potentially incorrect resutls for staging).
+ self.logger.info('{}/{} not published'.format(published[0],
published[1]))
+ return None
+
+ # Drop non-published repository information and thus reduce to boolean.
+ published = published is True
+
+ if simulate_merge:
+ # Restrict top layer archs to the whitelisted archs from merge
layer.
+ archs = set(target_archs(self.apiurl, project,
repository)).intersection(
+ set(self.target_archs(repository_pairs[1][0],
repository_pairs[1][1])))
+ else:
+ # Top of pseudometa file.
+ comment.append(state_hash)
+ archs = self.target_archs(project, repository)
+
+ if post_comments:
+ # Stores parsed install_check() results grouped by package.
+ self.package_results = {}
+
+ if not len(archs):
+ self.logger.debug('{} has no relevant
architectures'.format(project))
+ return None
+
+ result = True
+ for arch in archs:
+ directories = []
+ for pair_project, pair_repository in repository_pairs:
+ directories.append(self.mirror(pair_project, pair_repository,
arch))
+
+ if simulate_merge:
+ ignore = self.simulated_merge_ignore(repository_pairs[0],
repository_pairs[1], arch)
+ whitelist = self.binary_whitelist(repository_pairs[0],
repository_pairs[1], arch)
+
+ results = {
+ 'cycle': self.cycle_check(repository_pairs[0],
repository_pairs[1], arch),
+ 'install': self.install_check(project, directories,
repository_pairs[1][1], arch, ignore, whitelist),
+ }
+ else:
+ parse = project if post_comments else False
+ # Only products themselves will want no-filter or perhaps
+ # projects working on cleaning up a product.
+ no_filter = str2bool(Config.get(self.apiurl,
project).get('repo_checker-no-filter'))
+ results = {
+ 'cycle': CheckResult(True, None),
+ 'install': self.install_check(project, directories,
repository, arch,
+ parse=parse,
no_filter=no_filter),
+ }
+
+ if not all(result.success for _, result in results.items()):
+ # Not all checks passed, build comment.
+ result = False
+ self.result_comment(repository, arch, results, comment)
+
+ if simulate_merge:
+ info_extra = {'build': state_hash}
+ if not result:
+ # Some checks in group did not pass, post comment.
+ # Avoid identical comments with different build hash during
+ # target project build phase. Once published update regardless.
+ self.comment_write(state='seen', result='failed',
project=project,
+ message='\n'.join(comment).strip(),
identical=True,
+ info_extra=info_extra,
info_extra_identical=published)
+ else:
+ # Post passed comment only if previous failed comment.
+ text = 'Previously reported problems have been resolved.'
+ self.comment_write(state='done', result='passed',
project=project,
+ message=text, identical=True,
only_replace=True,
+ info_extra=info_extra)
+ else:
+ text = '\n'.join(comment).strip()
+ if not self.dryrun:
+ filename = self.project_pseudometa_file_name(project,
repository)
+ project_pseudometa_file_ensure(
+ self.apiurl, project, filename, text + '\n', 'repo_checker
project_only run')
+ else:
+ print(text)
+
+ if post_comments:
+ self.package_comments(project)
+
+ if result and not published:
+ # Wait for the complete stack to build before positive result.
+ self.logger.debug('demoting result from accept to ignore due to
non-published layer')
+ result = None
+
+ return result
+
+ @memoize(session=True)
+ def project_repository(self, project):
+ repository = Config.get(self.apiurl, project).get('main-repo')
+ if not repository:
+ self.logger.debug('no main-repo defined for {}'.format(project))
+
+ search_project = 'openSUSE:Factory'
+ for search_repository in ('snapshot', 'standard'):
+ repository = repository_path_search(
+ self.apiurl, project, search_project, search_repository)
+
+ if repository:
+ self.logger.debug('found chain to {}/{} via {}'.format(
+ search_project, search_repository, repository))
+ break
+
+ return repository
+
+ @memoize(ttl=60, session=True)
+ def request_repository_pairs(self, request, action):
+ repository = self.project_repository(action.tgt_project)
+ if not repository:
+ self.review_messages['declined'] =
ERROR_REPO_SPECIFIED.format(action.tgt_project)
+ return False
+
+ repository_pairs = []
+ # Assumes maintenance_release target project has staging disabled.
+ if Config.get(self.apiurl, action.tgt_project).get('staging'):
+ stage_info =
self.staging_api(action.tgt_project).packages_staged.get(action.tgt_package)
+ if not stage_info or str(stage_info['rq_id']) !=
str(request.reqid):
+ self.logger.info('{} not staged'.format(request.reqid))
+ return None
+
+ # Staging setup is convoluted and thus the repository setup does
not
+ # contain a path to the target project. Instead the ports
repository
+ # is used to import the target prjconf. As such the staging group
+ # repository must be explicitly layered on top of target project.
+ repository_pairs.append([stage_info['prj'], repository])
+ repository_pairs.extend(repository_path_expand(self.apiurl,
action.tgt_project, repository))
+ else:
+ # Find a repository which links to target project "main"
repository.
+ repository = repository_path_search(
+ self.apiurl, action.src_project, action.tgt_project,
repository)
+ if not repository:
+ self.review_messages['declined'] =
ERROR_REPO_SPECIFIED.format(action.tgt_project)
+ return False
+
+ repository_pairs.extend(repository_path_expand(self.apiurl,
action.src_project, repository))
+
+ return repository_pairs
+
def check_action_submit(self, request, action):
- if not self.ensure_group(request, action):
+ repository_pairs = self.request_repository_pairs(request, action)
+ if not isinstance(repository_pairs, list):
+ return repository_pairs
+
+ state_hash = self.repository_state(repository_pairs)
+ if not self.repository_check(repository_pairs, state_hash, True):
return None
self.review_messages['accepted'] = 'cycle and install check passed'
@@ -547,11 +564,30 @@
self.comment_write(state='seen', result='failed')
return None
- # Allow for delete to be declined before ensuring group passed.
- if not self.ensure_group(request, action):
+ repository_pairs = self.request_repository_pairs(request, action)
+ if not isinstance(repository_pairs, list):
+ return repository_pairs
+
+ state_hash = self.repository_state(repository_pairs)
+ if not self.repository_check(repository_pairs, state_hash, True):
return None
- self.review_messages['accepted'] = 'delete request is safe'
+ self.review_messages['accepted'] = 'cycle and install check passed'
+ return True
+
+ def check_action_maintenance_release(self, request, action):
+ # No reason to special case patchinfo since same source and target
+ # projects which is all that repo_checker cares about.
+
+ repository_pairs = self.request_repository_pairs(request, action)
+ if not isinstance(repository_pairs, list):
+ return repository_pairs
+
+ state_hash = self.repository_state(repository_pairs)
+ if not self.repository_check(repository_pairs, state_hash, True):
+ return None
+
+ self.review_messages['accepted'] = 'cycle and install check passed'
return True
@@ -566,7 +602,6 @@
parser.add_option('--skip-cycle', action='store_true', help='skip
cycle check')
parser.add_option('--force', action='store_true', help='force review
even if project is not ready')
- parser.add_option('--limit-group', metavar='GROUP', help='only review
requests in specific group')
return parser
@@ -577,7 +612,6 @@
bot.skip_cycle = self.options.skip_cycle
bot.force = self.options.force
- bot.limit_group = self.options.limit_group
return bot
++++++ openSUSE-release-tools.obsinfo ++++++
--- /var/tmp/diff_new_pack.nozftN/_old 2018-08-31 10:43:23.687127537 +0200
+++ /var/tmp/diff_new_pack.nozftN/_new 2018-08-31 10:43:23.687127537 +0200
@@ -1,5 +1,5 @@
name: openSUSE-release-tools
-version: 20180820.d7d5724
-mtime: 1534817024
-commit: d7d5724daefff290bbef79f37481a40ff6a1d57d
+version: 20180822.a9f1bc0
+mtime: 1534977191
+commit: a9f1bc0ee71e8d1307b4e756f220e0289a8bd17a