Issue tracker augmentation

If you find the issue tracking software you’re using lacks specific process or functionality, it may be possible to augment it through fairly straightforward use of its API.

#!/usr/bin/env python3

"""
    This script scans all issues in GitLab and performs arbitrary
    modifications. It's currently set up to ensure that every issue
    is assigned to at least one user, defaulting to a random choice
    between the project's owner and maintainers.

    This is just a demonstration of how you can augment an issue
    tracker's process with a simple cronjob. Left as an exercise to
    the reader is how one would go about doing this reactively with
    webhooks:

    https://docs.gitlab.com/ee/user/project/integrations/webhooks.html#issue-events
"""

import gitlab
import logging
import os
import random


ASSIGNEE_ACCESS_LEVELS = set([
    gitlab.MAINTAINER_ACCESS,
    gitlab.OWNER_ACCESS,
])


def get_active_project_members(gl, project):
    # The documentation (https://docs.gitlab.com/ee/api/members.html)
    # is unclear whether the state attribute of a membership object
    # represents the state of the membership or the state of the user.
    # To be safe I'm checking both, but we could investigate if it
    # refers to the latter and save a check in the process.
    return [
        member
        for member in project.members.list(as_list=False)
        if member.state == "active"
        and get_cached_user(gl, member.username).state == "active"
    ]


def get_default_assignees(gl, project):
    return [
        member
        for member in get_active_project_members(gl, project)
        if member.access_level in ASSIGNEE_ACCESS_LEVELS
    ]


def adjust_project_issues(gl, project):
    default_assignees = None
    for issue in project.issues.list(as_list=False):
        if not any((
            user["state"] == "active"
            for user in issue.assignees
        )):
            default_assignees = default_assignees or get_default_assignees(gl, project)
            assigned_user = random.choice(default_assignees)
            logger.info(f"Assigning {assigned_user.username} to {issue.web_url}")
            issue.assignee_id = get_cached_user(gl, assigned_user.username).id
            issue.save()


def get_cached_user(gl, username):
    if not hasattr(gl, "cached_users"):
        gl.cached_users = {}
    if username not in gl.cached_users:
        gl.cached_users[username] = gl.users.list(username=username)[0]
    return gl.cached_users[username]


if __name__ == "__main__":
    logging.basicConfig(level=os.environ.get("LOGLEVEL") or "INFO")
    logger = logging.getLogger()

    logger.debug(f"Connecting to {os.environ['GITLAB_URL']}")
    with gitlab.Gitlab(
        os.environ["GITLAB_URL"],
        private_token=os.environ["GITLAB_TOKEN"],
    ) as gl:
        gl.auth()

        for project in gl.projects.list(as_list=False):
            logger.debug(f"Considering {project.name}")
            adjust_project_issues(gl, project)