Merge pull request #1780 from jberry-suse/staging-report
openqa-comments: rework openQA report as status check report (and thus rename as well)
This commit is contained in:
commit
3310ec0c63
8
dist/package/openSUSE-release-tools.spec
vendored
8
dist/package/openSUSE-release-tools.spec
vendored
@ -467,7 +467,7 @@ exit 0
|
||||
%exclude %{_datadir}/%{source_dir}/metrics
|
||||
%exclude %{_datadir}/%{source_dir}/metrics.py
|
||||
%exclude %{_datadir}/%{source_dir}/metrics_release.py
|
||||
%exclude %{_bindir}/osrt-openqa-comments
|
||||
%exclude %{_bindir}/osrt-staging-report
|
||||
%exclude %{_datadir}/%{source_dir}/pkglistgen.py
|
||||
%exclude %{_datadir}/%{source_dir}/repo_checker.pl
|
||||
%exclude %{_datadir}/%{source_dir}/repo_checker.py
|
||||
@ -584,7 +584,7 @@ exit 0
|
||||
%files staging-bot
|
||||
%defattr(-,root,root,-)
|
||||
%{_bindir}/osrt-devel-project
|
||||
%{_bindir}/osrt-openqa-comments
|
||||
%{_bindir}/osrt-staging-report
|
||||
%{_bindir}/osrt-suppkg_rebuild
|
||||
%{_datadir}/%{source_dir}/devel-project.py
|
||||
%{_datadir}/%{source_dir}/suppkg_rebuild.py
|
||||
@ -594,8 +594,8 @@ exit 0
|
||||
%{_unitdir}/osrt-staging-bot-daily@.timer
|
||||
%{_unitdir}/osrt-staging-bot-devel-list.service
|
||||
%{_unitdir}/osrt-staging-bot-devel-list.timer
|
||||
%{_unitdir}/osrt-staging-bot-openqa-comments@.service
|
||||
%{_unitdir}/osrt-staging-bot-openqa-comments@.timer
|
||||
%{_unitdir}/osrt-staging-bot-staging-report@.service
|
||||
%{_unitdir}/osrt-staging-bot-staging-report@.timer
|
||||
%{_unitdir}/osrt-staging-bot-regular@.service
|
||||
%{_unitdir}/osrt-staging-bot-regular@.timer
|
||||
%{_unitdir}/osrt-staging-bot-reminder.service
|
||||
|
@ -1,208 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
import argparse
|
||||
from datetime import datetime, timedelta
|
||||
from collections import defaultdict
|
||||
import json
|
||||
|
||||
from osclib.comments import CommentAPI
|
||||
from osclib.conf import Config
|
||||
from osclib.stagingapi import StagingAPI
|
||||
|
||||
import osc
|
||||
|
||||
MARGIN_HOURS = 4
|
||||
MAX_LINES = 6
|
||||
|
||||
|
||||
class OpenQAReport(object):
|
||||
def __init__(self, api):
|
||||
self.api = api
|
||||
self.comment = CommentAPI(api.apiurl)
|
||||
|
||||
def _package_url(self, package):
|
||||
link = '/package/live_build_log/%s/%s/%s/%s'
|
||||
link = link % (package['project'],
|
||||
package['package'],
|
||||
package['repository'],
|
||||
package['arch'])
|
||||
text = '[%s](%s)' % (package['arch'], link)
|
||||
return text
|
||||
|
||||
def _openQA_url(self, job):
|
||||
test_name = job['name'].split('-')[-1]
|
||||
link = '%s/tests/%s' % (self.api.copenqa, job['id'])
|
||||
text = '[%s](%s)' % (test_name, link)
|
||||
return text
|
||||
|
||||
def _openQA_module_url(self, job, module):
|
||||
link = '%s/tests/%s/modules/%s/steps/1' % (
|
||||
self.api.copenqa, job['id'], module['name']
|
||||
)
|
||||
text = '[%s](%s)' % (module['name'], link)
|
||||
return text
|
||||
|
||||
def old_enough(self, _date):
|
||||
time_delta = datetime.utcnow() - _date
|
||||
safe_margin = timedelta(hours=MARGIN_HOURS)
|
||||
return safe_margin <= time_delta
|
||||
|
||||
def get_info(self, project):
|
||||
_prefix = '{}:'.format(self.api.cstaging)
|
||||
if project.startswith(_prefix):
|
||||
project = project.replace(_prefix, '')
|
||||
|
||||
query = {'format': 'json'}
|
||||
url = self.api.makeurl(('project', 'staging_projects',
|
||||
self.api.project, project), query=query)
|
||||
info = json.load(self.api.retried_GET(url))
|
||||
return info
|
||||
|
||||
def is_there_openqa_comment(self, project):
|
||||
"""Return True if there is a previous comment."""
|
||||
signature = '<!-- openQA status -->'
|
||||
comments = self.comment.get_comments(project_name=project)
|
||||
comment = [c for c in comments.values() if signature in c['comment']]
|
||||
return len(comment) > 0
|
||||
|
||||
def update_status_comment(self, project, report, force=False):
|
||||
signature = '<!-- openQA status -->'
|
||||
report = '%s\n%s' % (signature, str(report))
|
||||
|
||||
write_comment = False
|
||||
|
||||
comments = self.comment.get_comments(project_name=project)
|
||||
comment = [c for c in comments.values() if signature in c['comment']]
|
||||
if comment and len(comment) > 1:
|
||||
print 'ERROR. There are more than one openQA status comment in %s' % project
|
||||
# for c in comment:
|
||||
# self.comment.delete(c['id'])
|
||||
# write_comment = True
|
||||
elif comment and comment[0]['comment'] != report and self.old_enough(comment[0]['when']):
|
||||
self.comment.delete(comment[0]['id'])
|
||||
write_comment = True
|
||||
elif not comment:
|
||||
write_comment = True
|
||||
|
||||
if write_comment or force:
|
||||
if osc.conf.config['debug']:
|
||||
print 'Updating comment'
|
||||
self.comment.add_comment(project_name=project, comment=report)
|
||||
|
||||
def _report_broken_packages(self, info):
|
||||
broken_package_status = info['broken_packages']
|
||||
|
||||
# Group packages by name
|
||||
groups = defaultdict(list)
|
||||
for package in broken_package_status:
|
||||
groups[package['package']].append(package)
|
||||
|
||||
failing_lines = [
|
||||
'* Build failed %s (%s)' % (key, ', '.join(self._package_url(p) for p in value))
|
||||
for key, value in groups.iteritems()
|
||||
]
|
||||
|
||||
report = '\n'.join(failing_lines[:MAX_LINES])
|
||||
if len(failing_lines) > MAX_LINES:
|
||||
report += '* and more (%s) ...' % (len(failing_lines) - MAX_LINES)
|
||||
return report
|
||||
|
||||
def _report_openQA(self, info):
|
||||
failing_lines, green_lines = [], []
|
||||
|
||||
openQA_status = info['openqa_jobs']
|
||||
for job in openQA_status:
|
||||
test_name = job['name'].split('-')[-1]
|
||||
fails = [
|
||||
' * %s (%s)' % (test_name, self._openQA_module_url(job, module))
|
||||
for module in job['modules'] if module['result'] == 'failed'
|
||||
]
|
||||
|
||||
if fails:
|
||||
failing_lines.extend(fails)
|
||||
else:
|
||||
green_lines.append(self._openQA_url(job))
|
||||
|
||||
failing_report, green_report = '', ''
|
||||
if failing_lines:
|
||||
failing_report = '* Failing tests:\n' + '\n'.join(failing_lines[:MAX_LINES])
|
||||
if len(failing_lines) > MAX_LINES:
|
||||
failing_report += '\n * and more (%s) ...' % (len(failing_lines) - MAX_LINES)
|
||||
if green_lines:
|
||||
green_report = '* Succeeding tests:' + ', '.join(green_lines[:MAX_LINES])
|
||||
if len(green_lines) > MAX_LINES:
|
||||
green_report += ', and more (%s) ...' % (len(green_lines) - MAX_LINES)
|
||||
|
||||
return '\n'.join((failing_report, green_report)).strip(), bool(failing_lines)
|
||||
|
||||
def report(self, project):
|
||||
info = self.get_info(project)
|
||||
|
||||
# Some staging projects do not have info like
|
||||
# openSUSE:Factory:Staging:Gcc49
|
||||
if not info:
|
||||
return
|
||||
|
||||
if info['overall_state'] == 'empty':
|
||||
return
|
||||
|
||||
# The 'unacceptable' status means that the project will be
|
||||
# replaced soon. Better do not disturb with noise.
|
||||
if info['overall_state'] == 'unacceptable':
|
||||
return
|
||||
|
||||
report_broken_packages = self._report_broken_packages(info)
|
||||
report_openQA, some_openqa_fail = self._report_openQA(info)
|
||||
|
||||
if report_broken_packages or some_openqa_fail:
|
||||
if report_broken_packages:
|
||||
report_broken_packages = 'Broken:\n\n' + report_broken_packages
|
||||
if report_openQA:
|
||||
report_openQA = 'openQA:\n\n' + report_openQA
|
||||
report = '\n\n'.join((report_broken_packages, report_openQA))
|
||||
report = report.strip()
|
||||
if report:
|
||||
if osc.conf.config['debug']:
|
||||
print project
|
||||
print '-' * len(project)
|
||||
print report
|
||||
self.update_status_comment(project, report)
|
||||
elif not info['overall_state'] == 'acceptable' and self.is_there_openqa_comment(project):
|
||||
report = 'Congratulations! All fine now.'
|
||||
if osc.conf.config['debug']:
|
||||
print project
|
||||
print '-' * len(project)
|
||||
print report
|
||||
self.update_status_comment(project, report, force=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Command to publish openQA status in Staging projects')
|
||||
parser.add_argument('-s', '--staging', type=str, default=None,
|
||||
help='staging project letter')
|
||||
parser.add_argument('-f', '--force', action='store_true', default=False,
|
||||
help='force the write of the comment')
|
||||
parser.add_argument('-p', '--project', type=str, default='Factory',
|
||||
help='openSUSE version to make the check (Factory, 13.2)')
|
||||
parser.add_argument('-d', '--debug', action='store_true', default=False,
|
||||
help='enable debug information')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
osc.conf.get_config()
|
||||
osc.conf.config['debug'] = args.debug
|
||||
|
||||
if args.force:
|
||||
MARGIN_HOURS = 0
|
||||
|
||||
apiurl = osc.conf.config['apiurl']
|
||||
Config(apiurl, args.project)
|
||||
api = StagingAPI(apiurl, args.project)
|
||||
openQA = OpenQAReport(api)
|
||||
|
||||
if args.staging:
|
||||
openQA.report(api.prj_from_letter(args.staging))
|
||||
else:
|
||||
for staging in api.get_staging_projects():
|
||||
openQA.report(staging)
|
@ -156,6 +156,10 @@ class CommentAPI(object):
|
||||
|
||||
comment = self.truncate(comment.strip())
|
||||
|
||||
# OBS returns unicode from some APIs, but comment API does not accept
|
||||
# when included. Rather than handle everywhere just strip here.
|
||||
comment = comment.encode('ascii', 'ignore')
|
||||
|
||||
query = {}
|
||||
if parent_id:
|
||||
query['parent_id'] = parent_id
|
||||
|
162
staging-report.py
Executable file
162
staging-report.py
Executable file
@ -0,0 +1,162 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
import argparse
|
||||
from datetime import datetime, timedelta
|
||||
from collections import defaultdict
|
||||
import json
|
||||
|
||||
from osclib.comments import CommentAPI
|
||||
from osclib.conf import Config
|
||||
from osclib.stagingapi import StagingAPI
|
||||
|
||||
import osc
|
||||
|
||||
MARGIN_HOURS = 4
|
||||
MAX_LINES = 6
|
||||
MARKER = 'StagingReport'
|
||||
|
||||
|
||||
class StagingReport(object):
|
||||
def __init__(self, api):
|
||||
self.api = api
|
||||
self.comment = CommentAPI(api.apiurl)
|
||||
|
||||
def _package_url(self, package):
|
||||
link = '/package/live_build_log/%s/%s/%s/%s'
|
||||
link = link % (package['project'],
|
||||
package['package'],
|
||||
package['repository'],
|
||||
package['arch'])
|
||||
text = '[%s](%s)' % (package['arch'], link)
|
||||
return text
|
||||
|
||||
def old_enough(self, _date):
|
||||
time_delta = datetime.utcnow() - _date
|
||||
safe_margin = timedelta(hours=MARGIN_HOURS)
|
||||
return safe_margin <= time_delta
|
||||
|
||||
def update_status_comment(self, project, report, force=False, only_replace=False):
|
||||
report = self.comment.add_marker(report, MARKER)
|
||||
comments = self.comment.get_comments(project_name=project)
|
||||
comment, _ = self.comment.comment_find(comments, MARKER)
|
||||
if comment:
|
||||
write_comment = (report != comment['comment'] and self.old_enough(comment['when']))
|
||||
else:
|
||||
write_comment = not only_replace
|
||||
|
||||
if write_comment or force:
|
||||
if osc.conf.config['debug']:
|
||||
print 'Updating comment'
|
||||
if comment:
|
||||
self.comment.delete(comment['id'])
|
||||
self.comment.add_comment(project_name=project, comment=report)
|
||||
|
||||
def _report_broken_packages(self, info):
|
||||
broken_package_status = info['broken_packages']
|
||||
|
||||
# Group packages by name
|
||||
groups = defaultdict(list)
|
||||
for package in broken_package_status:
|
||||
groups[package['package']].append(package)
|
||||
|
||||
failing_lines = [
|
||||
'* Build failed %s (%s)' % (key, ', '.join(self._package_url(p) for p in value))
|
||||
for key, value in groups.iteritems()
|
||||
]
|
||||
|
||||
report = '\n'.join(failing_lines[:MAX_LINES])
|
||||
if len(failing_lines) > MAX_LINES:
|
||||
report += '* and more (%s) ...' % (len(failing_lines) - MAX_LINES)
|
||||
return report
|
||||
|
||||
def report_checks(self, info):
|
||||
failing_lines, green_lines = [], []
|
||||
|
||||
links_state = {}
|
||||
for check in info['checks']:
|
||||
links_state.setdefault(check['state'], [])
|
||||
links_state[check['state']].append('[{}]({})'.format(check['name'], check['url']))
|
||||
|
||||
lines = []
|
||||
failure = False
|
||||
for state, links in links_state.items():
|
||||
if len(links) > MAX_LINES:
|
||||
extra = len(links) - MAX_LINES
|
||||
links = links[:MAX_LINES]
|
||||
links.append('and {} more...'.format(extra))
|
||||
|
||||
lines.append('- {}'.format(state))
|
||||
if state != 'success':
|
||||
lines.extend([' - {}'.format(link) for link in links])
|
||||
failure = True
|
||||
else:
|
||||
lines[-1] += ': {}'.format(', '.join(links))
|
||||
|
||||
return '\n'.join(lines).strip(), failure
|
||||
|
||||
def report(self, project, aggregate=True, force=False):
|
||||
info = self.api.project_status(project, aggregate)
|
||||
|
||||
# Some staging projects do not have info like
|
||||
# openSUSE:Factory:Staging:Gcc49
|
||||
if not info:
|
||||
return
|
||||
|
||||
if info['overall_state'] == 'empty':
|
||||
return
|
||||
|
||||
# The 'unacceptable' status means that the project will be
|
||||
# replaced soon. Better do not disturb with noise.
|
||||
if info['overall_state'] == 'unacceptable':
|
||||
return
|
||||
|
||||
report_broken_packages = self._report_broken_packages(info)
|
||||
report_checks, check_failure = self.report_checks(info)
|
||||
|
||||
if report_broken_packages or check_failure:
|
||||
if report_broken_packages:
|
||||
report_broken_packages = 'Broken:\n\n' + report_broken_packages
|
||||
if report_checks:
|
||||
report_checks = 'Checks:\n\n' + report_checks
|
||||
report = '\n\n'.join((report_broken_packages, report_checks))
|
||||
report = report.strip()
|
||||
only_replace = False
|
||||
else:
|
||||
report = 'Congratulations! All fine now.'
|
||||
only_replace = True
|
||||
|
||||
self.update_status_comment(project, report, force=force, only_replace=only_replace)
|
||||
|
||||
if osc.conf.config['debug']:
|
||||
print project
|
||||
print '-' * len(project)
|
||||
print report
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Publish report on staging status as comment on staging project')
|
||||
parser.add_argument('-s', '--staging', type=str, default=None,
|
||||
help='staging project')
|
||||
parser.add_argument('-f', '--force', action='store_true', default=False,
|
||||
help='force a comment to be written')
|
||||
parser.add_argument('-p', '--project', type=str, default='openSUSE:Factory',
|
||||
help='project to check (ex. openSUSE:Factory, openSUSE:Leap:15.1)')
|
||||
parser.add_argument('-d', '--debug', action='store_true', default=False,
|
||||
help='enable debug information')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
osc.conf.get_config()
|
||||
osc.conf.config['debug'] = args.debug
|
||||
|
||||
apiurl = osc.conf.config['apiurl']
|
||||
Config(apiurl, args.project)
|
||||
api = StagingAPI(apiurl, args.project)
|
||||
staging_report = StagingReport(api)
|
||||
|
||||
if args.staging:
|
||||
staging_report.report(api.prj_from_letter(args.staging), False, args.force)
|
||||
else:
|
||||
for staging in api.get_staging_projects():
|
||||
staging_report.report(staging, True, args.force)
|
@ -1,11 +0,0 @@
|
||||
[Unit]
|
||||
Description=openSUSE Release Tools: staging-bot comment openQA/build results on stagings in %i
|
||||
|
||||
[Service]
|
||||
User=osrt-staging-bot
|
||||
SyslogIdentifier=osrt-staging-bot
|
||||
ExecStart=/usr/bin/osrt-openqa-comments -p "%i"
|
||||
RuntimeMaxSec=6 hour
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -1,10 +0,0 @@
|
||||
[Unit]
|
||||
Description=openSUSE Release Tools: staging-bot comment openQA/build results on stagings in %i
|
||||
|
||||
[Timer]
|
||||
OnBootSec=120
|
||||
OnUnitInactiveSec=3 min
|
||||
Unit=osrt-staging-bot-openqa-comments@%i.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
11
systemd/osrt-staging-bot-staging-report@.service
Normal file
11
systemd/osrt-staging-bot-staging-report@.service
Normal file
@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=openSUSE Release Tools: staging-bot report on staging status of %i
|
||||
|
||||
[Service]
|
||||
User=osrt-staging-bot
|
||||
SyslogIdentifier=osrt-staging-bot
|
||||
ExecStart=/usr/bin/osrt-staging-report --debug -p "%i"
|
||||
RuntimeMaxSec=6 hour
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
10
systemd/osrt-staging-bot-staging-report@.timer
Normal file
10
systemd/osrt-staging-bot-staging-report@.timer
Normal file
@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=openSUSE Release Tools: staging-bot report on staging status of %i
|
||||
|
||||
[Timer]
|
||||
OnBootSec=120
|
||||
OnUnitInactiveSec=3 min
|
||||
Unit=osrt-staging-bot-staging-report@%i.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
Loading…
x
Reference in New Issue
Block a user