#!/usr/bin/env python # -*- coding: utf-8 -*- # This file is part of Cockpit. # # Copyright (C) 2015 Red Hat, Inc. # # Cockpit is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation; either version 2.1 of the License, or # (at your option) any later version. # # Cockpit is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with Cockpit; If not, see . BASELINE_PRIORITY = 10 BRANCHES = [ 'master', 'rhel-7.4', 'rhel-7.3.1', 'rhel-7.3.2', 'rhel-7.3.3', 'rhel-7.3.4', 'rhel-7.3.5', 'rhel-7.3.6' ] DEFAULT_VERIFY = { 'avocado/fedora-24': BRANCHES, 'avocado/fedora-25': [ ], 'container/kubernetes': BRANCHES, 'selenium/firefox': BRANCHES, 'selenium/chrome': BRANCHES, 'verify/centos-7': BRANCHES, 'verify/continuous-atomic': [ ], 'verify/debian-stable': [ 'master' ], 'verify/debian-testing': [ 'master' ], 'verify/fedora-24': [ ], 'verify/fedora-25': [ ], 'verify/fedora-i386': [ 'master', 'rhel-7.4', 'rhel-7.3.6', 'rhel-7.3.5' ], 'verify/fedora-26': [ 'master' ], 'verify/fedora-atomic': BRANCHES, 'verify/fedora-testing': [ ], 'verify/ubuntu-1604': [ 'master' ], 'verify/ubuntu-stable': [ 'master' ], } # Non-public images used for testing REDHAT_VERIFY = { "verify/rhel-7": [ 'master', 'rhel-7.3.6', 'rhel-7.3.5', 'rhel-7.3.4', 'rhel-7.3.3', 'rhel-7.3.2' ], "verify/rhel-7-4": [ 'master', 'rhel-7.4' ], "verify/rhel-atomic": [ 'master' ], } # Server to tell us if we can test Red Hat images REDHAT_PING = "http://cockpit-11.e2e.bos.redhat.com" import argparse import os import json import pipes import random import sys import time import urllib sys.dont_write_bytecode = True from task import github def main(): parser = argparse.ArgumentParser(description='Bot: scan and update status of pull requests on GitHub') parser.add_argument('-v', '--human-readable', action="store_true", default=False, help='Display human readable output rather than tasks') parser.add_argument('-d', '--dry', action="store_true", default=False, help='Don''t actually change anything on GitHub') opts = parser.parse_args() api = github.GitHub() try: results = scan(api, not opts.dry, opts.human_readable) except RuntimeError, ex: sys.stderr.write("tests-scan: " + str(ex) + "\n") return 1 for result in results: if result: sys.stdout.write(result + "\n") return 0 # Prepare a human readable output def tests_human(priority, name, revision, ref, context, base=None): if priority is None: return try: priority = int(priority) except (ValueError, TypeError): pass return "{name:11} {context:25} {revision:10} {priority}".format( priority=priority, revision=revision[0:7], context=context, name=name ) # Prepare an test invocation command def tests_invoke(priority, name, revision, ref, context, base=None): try: priority = int(priority) except (ValueError, TypeError): priority = 0 if priority <= 0: return current = time.strftime('%Y%m%d-%H%M%M') cmd = "PRIORITY={priority:04d} TEST_NAME='{name}-{current}' TEST_REVISION='{revision}' bots/tests-invoke" if base: cmd += " --rebase='{base}'" cmd += " '{context}' '{ref}'" return cmd.format( priority=priority, name=pipes.quote(name), revision=pipes.quote(revision), base=pipes.quote(str(base)), ref=pipes.quote(ref), context=pipes.quote(context), current=current, ) def prioritize(status, title, labels, priority, context): state = status.get("state", None) update = { "state": "pending" } # This commit definitively succeeded or failed if state in [ "success", "failure" ]: priority = None update = None # This test errored, we try again but low priority elif state in [ "error" ]: priority -= 6 elif state in [ "pending" ]: update = None if priority > 0: if "priority" in labels(): priority += 2 if "blocked" in labels(): priority -= 1 # Pull requests where the title starts with WIP get penalized if title.startswith("WIP"): priority -= 3 # Is testing already in progress? description = status.get("description", "") if description.startswith(github.TESTING): priority = description update = None # Prefer "local" operating system elif os.environ.get("TEST_OS", "?") not in context: priority -= random.randint(1, 2) if update: if priority <= 0: update = None else: update["description"] = github.NOT_TESTED return [priority, update] def dict_is_subset(full, check): for (key, value) in check.items(): if not key in full or full[key] != value: return False return True def scan_for_pull_tasks(api, update, human, policy): contexts = policy results = [] branch_contexts = { } for (context, branches) in contexts.items(): for branch in branches: if branch not in branch_contexts: branch_contexts[branch] = [ ] branch_contexts[branch].append(context) def update_status(revision, context, last, changes): if changes: changes["context"] = context if update and changes and not dict_is_subset(last, changes): response = api.post("statuses/" + revision, changes, accept=[ 422 ]) # 422 Unprocessable Entity errors = response.get("errors", None) if not errors: return True for error in response.get("errors", []): sys.stderr.write("{0}: {1}\n".format(revision, error.get('message', json.dumps(error)))) sys.stderr.write(json.dumps(changes)) return False return True for branch in branch_contexts: ref = api.get("git/refs/heads/{0}".format(branch)) # if the branch doesn't exist but other branches begin with this name, GitHub returns a list # we don't want to use such a list # https://developer.github.com/v3/git/refs/ if ref and not isinstance(ref, list): revision = ref["object"]["sha"] statuses = api.statuses(revision) which = statuses.keys() or branch_contexts[branch] for context in which: status = statuses.get(context, { }) (priority, changes) = prioritize(status, "", lambda: [], 8, context) if update_status(revision, context, status, changes): results.append((priority, branch, revision, branch, context, None)) whitelist = github.whitelist() for pull in api.pulls(): title = pull["title"] number = pull["number"] revision = pull["head"]["sha"] statuses = api.statuses(revision) login = pull["head"]["user"]["login"] base = pull["base"]["ref"] # The branch this pull request targets def labels(): if "labels" not in pull: result = [ ] for label in api.get("issues/{0}/labels".format(number)): result.append(label["name"]) pull["labels"] = result return pull["labels"] # Do we have any statuses for this commit? have = len(statuses.keys()) > 0 for context in contexts: status = statuses.get(context, None) baseline = BASELINE_PRIORITY # modify the baseline slightly to favor older pull requests, so that we don't # end up with a bunch of half tested pull requests baseline += 1.0 - (min(100000, float(number)) / 100000) # Only create new status for requests that have none if not status: if have or context not in branch_contexts.get(base, []): continue status = { } # For unmarked and untested status, user must be in whitelist # Not this only applies to this specific commit. A new status # will apply if the user pushes a new commit. if login not in whitelist and status.get("description", github.NO_TESTING) == github.NO_TESTING: priority = github.NO_TESTING changes = { "description": github.NO_TESTING, "context": context, "state": "pending" } else: (priority, changes) = prioritize(status, title, labels, baseline, context) if update_status(revision, context, status, changes): results.append((priority, "pull-%d" % number, revision, "pull/%d/head" % number, context, base)) if human: func = lambda x: tests_human(*x) results.sort(reverse=True) else: func = lambda x: tests_invoke(*x) return map(func, results) def scan(api, update, human): kvm = os.access("/dev/kvm", os.R_OK | os.W_OK) if not kvm: sys.stderr.write("tests-scan: No /dev/kvm access, not running tests here\n") return [] policy = DEFAULT_VERIFY # Check if we have access to Red Hat network try: urllib.urlopen(REDHAT_PING).read() policy.update(REDHAT_VERIFY) except IOError: pass return scan_for_pull_tasks(api, update, human, policy) if __name__ == '__main__': sys.exit(main())