def test_restclient(self):
        """Test use of RestClient."""
        if self.skip_github_tests:
            print("Skipping test_restclient, no GitHub token available?")
            return

        client = RestClient('https://api.github.com', username=GITHUB_TEST_ACCOUNT, token=self.github_token)

        status, body = client.repos['easybuilders']['testrepository'].contents.a_directory['a_file.txt'].get()
        self.assertEqual(status, 200)
        # base64.b64encode requires & produces a 'bytes' value in Python 3,
        # but we need a string value hence the .decode() (also works in Python 2)
        self.assertEqual(body['content'].strip(), base64.b64encode(b'this is a line of text\n').decode())

        status, headers = client.head()
        self.assertEqual(status, 200)
        self.assertTrue(headers)
        self.assertTrue('X-GitHub-Media-Type' in headers)

        httperror_hit = False
        try:
            status, body = client.user.emails.post(body='*****@*****.**')
            self.assertTrue(False, 'posting to unauthorized endpoint did not throw a http error')
        except HTTPError:
            httperror_hit = True
        self.assertTrue(httperror_hit, "expected HTTPError not encountered")

        httperror_hit = False
        try:
            status, body = client.user.emails.delete(body='*****@*****.**')
            self.assertTrue(False, 'deleting to unauthorized endpoint did not throw a http error')
        except HTTPError:
            httperror_hit = True
        self.assertTrue(httperror_hit, "expected HTTPError not encountered")
Exemple #2
0
def main():
    types = {
        'dump': dump_data,
        'html': gen_pr_overview_page,
        'plot': plot_pr_stats,
        'print': pr_overview,
    }

    opts = {
        'github-account': ("GitHub account where repository is located", None, 'store', 'easybuilders', 'a'),
        'github-user': ("GitHub user to use (for authenticated access)", None, 'store', 'boegel', 'u'),
        'range': ("Range for PRs to take into account", None, 'store', None, 'x'),
        'repository': ("Repository to use", None, 'store', 'easybuild-easyconfigs', 'r'),
        'since': ("Date to use to select range of issues for which to pull in data (e.g. 2019-10-24)",
                  None, 'store', None, 's'),
        'type': ("Type of overview: 'dump', 'plot', 'print', or 'html'", 'choice',
                 'store_or_None', 'print', list(types.keys()), 't'),
        'update': ("Update existing data", None, 'store_true', False),
    }

    go = simple_option(go_dict=opts, descr="Script to print overview of pull requests for a GitHub repository")

    github_account = go.options.github_account
    github_user = go.options.github_user

    pr_range = None
    if go.options.range:
        print(go.options.range)
        range_regex = re.compile('^[0-9]+-[0-9]+$')
        if range_regex.match(go.options.range):
            pr_range = go.options.range.split('-')
        else:
            sys.stderr.write("Range '%s' does not match pattern '%s'\n" % (go.options.range, range_regex.pattern))
            sys.exit(1)

    github_token = fetch_github_token(github_user)
    github = RestClient(GITHUB_API_URL, username=github_user, token=github_token, user_agent='eb-pr-overview')

    pickle_file = None
    if go.args:
        pickle_file = go.args[0]

    downloading_msg = "Downloading PR data for %s/%s repo..." % (github_account, go.options.repository)
    print(downloading_msg)
    prs = fetch_prs_data(pickle_file, github, github_account, go.options.repository, downloading_msg,
                         pr_range=pr_range, update=go.options.update, since=go.options.since)

    if go.options.type in types:
        types[go.options.type](prs, go)
Exemple #3
0
def main():
    """the main function"""
    fancylogger.logToScreen(enable=True, stdout=True)
    fancylogger.setLogLevelInfo()

    options = {
        'github-user':
        ('Your github username to use', None, 'store', None, 'g'),
        'closed-pr': ('Delete all gists from closed pull-requests', None,
                      'store_true', True, 'p'),
        'all':
        ('Delete all gists from Easybuild ', None, 'store_true', False, 'a'),
        'orphans': ('Delete all gists without a pull-request', None,
                    'store_true', False, 'o'),
        'dry-run':
        ("Only show which gists will be deleted but don't actually delete them",
         None, 'store_true', False),
    }

    go = simple_option(options)
    log = go.log

    if not (go.options.all or go.options.closed_pr or go.options.orphans):
        raise EasyBuildError("Please tell me what to do?")

    if go.options.github_user is None:
        EasyBuildOptions.DEFAULT_LOGLEVEL = None  # Don't overwrite log level
        eb_go = EasyBuildOptions(envvar_prefix='EASYBUILD', go_args=[])
        username = eb_go.options.github_user
        log.debug("Fetch github username from easybuild, found: %s", username)
    else:
        username = go.options.github_user

    if username is None:
        raise EasyBuildError("Could not find a github username")
    else:
        log.info("Using username = %s", username)

    token = fetch_github_token(username)

    gh = RestClient(GITHUB_API_URL, username=username, token=token)

    all_gists = []
    cur_page = 1
    while True:
        status, gists = gh.gists.get(per_page=100, page=cur_page)

        if status != HTTP_STATUS_OK:
            raise EasyBuildError(
                "Failed to get a lists of gists for user %s: error code %s, message = %s",
                username, status, gists)
        if gists:
            all_gists.extend(gists)
            cur_page += 1
        else:
            break

    log.info("Found %s gists", len(all_gists))
    re_eb_gist = re.compile(
        r"(EasyBuild test report|EasyBuild log for failed build)(.*?)$")
    re_pr_nr = re.compile(r"(EB )?PR #([0-9]+)")

    pr_cache = {}
    num_deleted = 0

    for gist in all_gists:
        if not gist["description"]:
            continue

        gist_match = re_eb_gist.search(gist["description"])

        if not gist_match:
            log.debug("Found a non-Easybuild gist (id=%s)", gist["id"])
            continue

        log.debug("Found an Easybuild gist (id=%s)", gist["id"])

        pr_data = gist_match.group(2)

        pr_nrs_matches = re_pr_nr.findall(pr_data)

        if go.options.all:
            delete_gist = True
        elif not pr_nrs_matches:
            log.debug("Found Easybuild test report without PR (id=%s).",
                      gist["id"])
            delete_gist = go.options.orphans
        elif go.options.closed_pr:
            # All PRs must be closed
            delete_gist = True
            for pr_nr_match in pr_nrs_matches:
                eb_str, pr_num = pr_nr_match
                if eb_str or GITHUB_EASYBLOCKS_REPO in pr_data:
                    repo = GITHUB_EASYBLOCKS_REPO
                else:
                    repo = GITHUB_EASYCONFIGS_REPO

                cache_key = "%s-%s" % (repo, pr_num)

                if cache_key not in pr_cache:
                    try:
                        status, pr = gh.repos[GITHUB_EB_MAIN][repo].pulls[
                            pr_num].get()
                    except HTTPError as e:
                        status, pr = e.code, e.msg
                    if status != HTTP_STATUS_OK:
                        raise EasyBuildError(
                            "Failed to get pull-request #%s: error code %s, message = %s",
                            pr_num, status, pr)
                    pr_cache[cache_key] = pr["state"]

                if pr_cache[cache_key] == "closed":
                    log.debug("Found report from closed %s PR #%s (id=%s)",
                              repo, pr_num, gist["id"])
                elif delete_gist:
                    if len(pr_nrs_matches) > 1:
                        log.debug(
                            "Found at least 1 PR, that is not closed yet: %s/%s (id=%s)",
                            repo, pr_num, gist["id"])
                    delete_gist = False
        else:
            delete_gist = True

        if delete_gist:
            if go.options.dry_run:
                log.info("DRY-RUN: Delete gist with id=%s", gist["id"])
                num_deleted += 1
                continue
            try:
                status, del_gist = gh.gists[gist["id"]].delete()
            except HTTPError as e:
                status, del_gist = e.code, e.msg
            except URLError as e:
                status, del_gist = None, e.reason

            if status != HTTP_DELETE_OK:
                log.warning(
                    "Unable to remove gist (id=%s): error code %s, message = %s",
                    gist["id"], status, del_gist)
            else:
                log.info("Deleted gist with id=%s", gist["id"])
                num_deleted += 1

    if go.options.dry_run:
        log.info("DRY-RUN: Would delete %s gists", num_deleted)
    else:
        log.info("Deleted %s gists", num_deleted)
def main():
    """the main function"""
    fancylogger.logToScreen(enable=True, stdout=True)
    fancylogger.setLogLevelInfo()

    options = {
        'github-user': ('Your github username to use', None, 'store', None, 'g'),
        'closed-pr': ('Delete all gists from closed pull-requests', None, 'store_true', True, 'p'),
        'all': ('Delete all gists from Easybuild ', None, 'store_true', False, 'a'),
        'orphans': ('Delete all gists without a pull-request', None, 'store_true', False, 'o'),
    }

    go = simple_option(options)
    log = go.log

    if not (go.options.all or go.options.closed_pr or go.options.orphans):
        raise EasyBuildError("Please tell me what to do?")

    if go.options.github_user is None:
        eb_go = EasyBuildOptions(envvar_prefix='EASYBUILD', go_args=[])
        username = eb_go.options.github_user
        log.debug("Fetch github username from easybuild, found: %s", username)
    else:
        username = go.options.github_user

    if username is None:
        raise EasyBuildError("Could not find a github username")
    else:
        log.info("Using username = %s", username)

    token = fetch_github_token(username)

    gh = RestClient(GITHUB_API_URL, username=username, token=token)

    all_gists = []
    cur_page = 1
    while True:
        status, gists = gh.gists.get(per_page=100, page=cur_page)

        if status != HTTP_STATUS_OK:
            raise EasyBuildError("Failed to get a lists of gists for user %s: error code %s, message = %s",
                                 username, status, gists)
        if gists:
            all_gists.extend(gists)
            cur_page += 1
        else:
            break

    log.info("Found %s gists", len(all_gists))
    regex = re.compile(r"(EasyBuild test report|EasyBuild log for failed build).*?(?:PR #(?P<PR>[0-9]+))?\)?$")

    pr_cache = {}
    num_deleted = 0

    for gist in all_gists:
        if not gist["description"]:
            continue
        re_pr_num = regex.search(gist["description"])
        delete_gist = False

        if re_pr_num:
            log.debug("Found a Easybuild gist (id=%s)", gist["id"])
            pr_num = re_pr_num.group("PR")
            if go.options.all:
                delete_gist = True
            elif pr_num and go.options.closed_pr:
                log.debug("Found Easybuild test report for PR #%s", pr_num)

                if pr_num not in pr_cache:
                    status, pr = gh.repos[GITHUB_EB_MAIN][GITHUB_EASYCONFIGS_REPO].pulls[pr_num].get()
                    if status != HTTP_STATUS_OK:
                        raise EasyBuildError("Failed to get pull-request #%s: error code %s, message = %s",
                                             pr_num, status, pr)
                    pr_cache[pr_num] = pr["state"]

                if pr_cache[pr_num] == "closed":
                    log.debug("Found report from closed PR #%s (id=%s)", pr_num, gist["id"])
                    delete_gist = True

            elif not pr_num and go.options.orphans:
                log.debug("Found Easybuild test report without PR (id=%s)", gist["id"])
                delete_gist = True

        if delete_gist:
            status, del_gist = gh.gists[gist["id"]].delete()

            if status != HTTP_DELETE_OK:
                raise EasyBuildError("Unable to remove gist (id=%s): error code %s, message = %s",
                                     gist["id"], status, del_gist)
            else:
                log.info("Delete gist with id=%s", gist["id"])
                num_deleted += 1

    log.info("Deleted %s gists", num_deleted)
Exemple #5
0
def main():

    opts = {
        'core-cnt':
        ("Default core count to use for jobs", None, 'store', None),
        'github-account': ("GitHub account where repository is located", None,
                           'store', 'easybuilders', 'a'),
        'github-user': ("GitHub user to use (for authenticated access)", None,
                        'store', 'boegel', 'u'),
        'mode': ("Mode to run in", 'choice', 'store', MODE_CHECK_TRAVIS,
                 [MODE_CHECK_GITHUB_ACTIONS, MODE_CHECK_TRAVIS, MODE_TEST_PR]),
        'owner':
        ("Owner of the bot account that is used", None, 'store', 'boegel'),
        'repository': ("Repository to use", None, 'store',
                       'easybuild-easyconfigs', 'r'),
        'host':
        ("Label for current host (used to filter comments asking to test a PR)",
         None, 'store', ''),
        'pr-test-cmd':
        ("Command to use for testing easyconfig pull requests (should include '%(pr)s' template value)",
         None, 'store', ''),
    }

    go = simple_option(go_dict=opts)
    init_build_options()

    github_account = go.options.github_account
    github_user = go.options.github_user
    mode = go.options.mode
    owner = go.options.owner
    owner = go.options.owner
    repository = go.options.repository
    host = go.options.host
    pr_test_cmd = go.options.pr_test_cmd
    core_cnt = go.options.core_cnt

    github_token = fetch_github_token(github_user)

    # prepare using GitHub API
    github = RestClient(GITHUB_API_URL,
                        username=github_user,
                        token=github_token,
                        user_agent='eb-pr-check')

    if mode in [MODE_CHECK_GITHUB_ACTIONS, MODE_CHECK_TRAVIS]:

        if mode == MODE_CHECK_TRAVIS:
            res = fetch_travis_failed_builds(github_account, repository, owner,
                                             github_token)
        elif mode == MODE_CHECK_GITHUB_ACTIONS:
            res = fetch_github_failed_workflows(github, github_account,
                                                repository, github_user, owner)
        else:
            error("Unknown mode: %s" % mode)

        for pr, pr_comment, check_msg in res:
            params = {'per_page': GITHUB_MAX_PER_PAGE}
            pr_data, _ = fetch_pr_data(pr,
                                       github_account,
                                       repository,
                                       github_user,
                                       full=True,
                                       **params)
            if pr_data['state'] == GITHUB_PR_STATE_OPEN:
                comment(github,
                        github_user,
                        repository,
                        pr_data,
                        pr_comment,
                        check_msg=check_msg,
                        verbose=DRY_RUN)
            else:
                print("Not posting comment in already closed %s PR #%s" %
                      (repository, pr))

    elif mode == MODE_TEST_PR:
        if not host:
            error("--host is required when using '--mode %s' !" % MODE_TEST_PR)

        if '%(pr)s' not in pr_test_cmd or '%(eb_args)s' not in pr_test_cmd:
            error(
                "--pr-test-cmd should include '%%(pr)s' and '%%(eb_args)s', found '%s'"
                % (pr_test_cmd))

        if core_cnt is None:
            error(
                "--core-cnt must be used to specify the default number of cores to request per submitted job!"
            )

        notifications = check_notifications(github, github_user,
                                            github_account, repository)
        process_notifications(notifications, github, github_user,
                              github_account, repository, host, pr_test_cmd,
                              core_cnt)
    else:
        error("Unknown mode: %s" % mode)
Exemple #6
0
def fetch_github_failed_workflows(github, github_account, repository,
                                  github_user, owner):
    """Scan GitHub Actions for failed workflow runs."""

    res = []

    # only consider failed workflows triggered by pull requests
    params = {
        'event': 'pull_request',
        # filtering based on status='failure' no longer works correctly?!
        # also with status='completed' some workflow runs are not included in result...
        # 'status': 'failure',
        'per_page': GITHUB_MAX_PER_PAGE,
    }

    try:
        status, run_data = github.repos[github_account][
            repository].actions.runs.get(**params)
    except socket.gaierror as err:
        error("Failed to download GitHub Actions workflow runs data: %s" % err)

    if status == 200:
        run_data = list(run_data['workflow_runs'])
        print("Found %s failed workflow runs for %s/%s" %
              (len(run_data), github_account, repository))
    else:
        error(
            "Status for downloading GitHub Actions workflow runs data should be 200, got %s"
            % status)

    failing_prs = set()

    for idx, entry in enumerate(run_data):

        if entry['status'] != 'completed':
            print("Ignoring incomplete workflow run %s" % entry['html_url'])
            continue

        if entry['conclusion'] == 'success':
            print("Ignoring successful workflow run %s" % entry['html_url'])
            continue

        head_user = entry['head_repository']['owner']['login']
        head = '%s:%s' % (head_user, entry['head_branch'])
        head_sha = entry['head_sha']

        # determine corresponding PR (if any)
        status, pr_data = github.repos[github_account][repository].pulls.get(
            head=head)
        if status != 200:
            error(
                "Status for downloading data for PR with head %s should be 200, got %s"
                % (head, status))

        if len(pr_data) == 1:
            pr_data = pr_data[0]
            print("Failed workflow run %s found (PR: %s)" %
                  (entry['html_url'], pr_data['html_url']))

            pr_id = pr_data['number']

            # skip PRs for which a failing workflow was already encountered
            if pr_id in failing_prs:
                print("PR #%s already encountered, so skipping workflow %s" %
                      (pr_id, entry['html_url']))
                continue

            pr_data, _ = fetch_pr_data(pr_id,
                                       github_account,
                                       repository,
                                       github_user,
                                       full=True,
                                       per_page=GITHUB_MAX_PER_PAGE)

            if pr_data['state'] == 'open':

                pr_head_sha = pr_data['head']['sha']

                # make sure workflow was run for latest commit in this PR
                if head_sha != pr_head_sha:
                    msg = "Workflow %s was for commit %s, " % (
                        entry['html_url'], head_sha)
                    msg += "not latest commit in PR #%s (%s), so skipping" % (
                        pr_id, pr_head_sha)
                    print(msg)
                    continue

                # check status of most recent commit in this PR,
                # ignore this PR if status is "success" or "pending"
                pr_status = pr_data['status_last_commit']
                print("Status of last commit (%s) in PR #%s: %s" %
                      (pr_head_sha, pr_id, pr_status))

                if pr_status in [
                        'action_required', STATUS_PENDING, STATUS_SUCCESS
                ]:
                    print(
                        "Status of last commit in PR #%s is '%s', so ignoring it for now..."
                        % (pr_id, pr_status))
                    continue

                # download list of jobs in workflow
                run_id = entry['id']
                status, jobs_data = github.repos[github_account][
                    repository].actions.runs[run_id].jobs.get()
                if status != 200:
                    error(
                        "Failed to download list of jobs for workflow run %s" %
                        entry['html_url'])

                # determine ID of first failing job
                job_id = None
                for job in jobs_data['jobs']:
                    if job['conclusion'] == 'failure':
                        job_id = job['id']
                        print("Found failing job for workflow %s: %s" %
                              (entry['html_url'], job_id))
                        break

                if job_id is None:
                    error("ID of failing job not found for workflow %s" %
                          entry['html_url'])

                try:
                    status, log_txt = github.repos[github_account][
                        repository].actions.jobs[job_id].logs.get()
                except HTTPError as err:
                    status = err.code

                if status == 200:
                    print("Downloaded log for job %s" % job_id)
                else:
                    warning("Failed to download log for job %s" % job_id)
                    log_txt = '(failed to fetch log contents due to HTTP status code %s)' % status

                # strip off timestamp prefixes
                # example timestamp: 2020-07-13T09:54:36.5004935Z
                timestamp_regex = re.compile(
                    r'^[0-9-]{10}T[0-9:]{8}\.[0-9]+Z ')
                log_lines = [
                    timestamp_regex.sub('', x) for x in log_txt.splitlines()
                ]

                # determine line that marks end of output for failing test suite:
                # "ERROR: Not all tests were successful"
                error_line_idx = None
                for idx, line in enumerate(log_lines):
                    if line.startswith("ERROR: Not all tests were successful"):
                        error_line_idx = idx
                        print("Found error line @ index %s" % error_line_idx)
                        break

                if error_line_idx is None:
                    log_txt_clean = '\n'.join(log_lines)
                    warning(
                        "Log line that marks end of test suite output not found for job %s!\n%s"
                        % (job_id, log_txt_clean))
                    if is_fluke(log_txt):
                        owner_gh_token = fetch_github_token(owner)
                        if owner_gh_token:
                            github_owner = RestClient(GITHUB_API_URL,
                                                      username=owner,
                                                      token=owner_gh_token,
                                                      user_agent='eb-pr-check')
                            print(
                                "Fluke found, restarting this workflow using @%s's GitHub account..."
                                % owner)
                            repo_api = github_owner.repos[github_account][
                                repository]
                            status, jobs_data = repo_api.actions.runs[
                                run_id].rerun.post()
                            if status == 201:
                                print("Workflow %s restarted" %
                                      entry['html_url'])
                            else:
                                print(
                                    "Failed to restart workflow %s: status %s"
                                    % (entry['html_url'], status))
                        else:
                            warning(
                                "Fluke found but can't restart workflow, no token found for @%s"
                                % owner)

                    continue

                # find line that marks start of test output: only dots and 'E'/'F' characters
                start_test_regex = re.compile(r'^[\.EF]+$')
                start_line_idx = error_line_idx
                start_log_line = log_lines[start_line_idx]
                while (start_line_idx >= 0
                       and not (start_log_line
                                and start_test_regex.match(start_log_line))):
                    start_line_idx -= 1
                    start_log_line = log_lines[start_line_idx]

                log_lines = log_lines[start_line_idx + 1:error_line_idx + 1]

                # compose comment
                pr_comment = "@%s: Tests failed in GitHub Actions" % pr_data[
                    'user']['login']
                pr_comment += ", see %s" % entry['html_url']

                # use first part of comment to check whether comment was already posted
                check_msg = pr_comment

                if len(log_lines) > 100:
                    log_lines = log_lines[-100:]
                    pr_comment += "\nLast 100 lines of output from first failing test suite run:\n\n```"
                else:
                    pr_comment += "\nOutput from first failing test suite run:\n\n```"

                for line in log_lines:
                    pr_comment += line + '\n'

                pr_comment += "```\n"

                pr_comment += "\n*bleep, bloop, I'm just a bot (boegelbot v%s)*\n" % VERSION
                pr_comment += "Please talk to my owner `@%s` if you notice you me acting stupid),\n" % owner
                pr_comment += "or submit a pull request to https://github.com/boegel/boegelbot fix the problem."

                res.append((pr_id, pr_comment, check_msg))
                failing_prs.add(pr_id)
            else:
                print("Ignoring failed workflow run for closed PR %s" %
                      pr_data['html_url'])
        else:
            warning("Expected exactly one PR with head %s, found %s: %s" %
                    (head, len(pr_data), pr_data))

    print("Processed %d failed workflow runs, found %d PRs to report back on" %
          (len(run_data), len(res)))

    return res