Exemple #1
0
class TriageIssues(DefaultTriager):

    VALID_COMMANDS = ['needs_info', '!needs_info', 'notabug', 
                      'wontfix', 'bug_resolved', 'resolved_by_pr', 
                      'needs_contributor', 'duplicate_of']

    def run(self):
        """Starts a triage run"""
        self.repo = self._connect().get_repo("ansible/ansible-modules-%s" %
                                        self.github_repo)

        if self.number:
            issue = self.repo.get_issue(int(self.number))
            self.issue = IssueWrapper(repo=self.repo, issue=issue)
            self.issue.get_events()
            self.issue.get_comments()
            self.process()
        else:

            last_run = None
            now = self.get_current_time()
            #import epdb; epdb.st()
            last_run_file = '~/.ansibullbot/cache'
            last_run_file += '/ansible/ansible-modules-%s/' % self.github_repo
            last_run_file += 'issues/last_run.pickle'
            last_run_file = os.path.expanduser(last_run_file)
            if os.path.isfile(last_run_file):
                try:
                    with open(last_run_file, 'rb') as f:
                        last_run = pickle.load(f)
                except Exception as e:
                    print(e)
            #import epdb; epdb.st()

            if last_run:
                issues = self.repo.get_issues(since=last_run)
            else:
                issues = self.repo.get_issues()

            for issue in issues:
                if self.start_at and issue.number > self.start_at:
                    continue
                if self.is_pr(issue):
                    continue
                self.issue = IssueWrapper(repo=self.repo, issue=issue)
                self.issue.get_events()
                self.issue.get_comments()
                action_res = self.process()
                if action_res:
                    while action_res['REDO']:
                        issue = self.repo.get_issue(int(issue.number))
                        self.issue = IssueWrapper(repo=self.repo, issue=issue)
                        self.issue.get_events()
                        self.issue.get_comments()
                        action_res = self.process()
                        if not action_res:
                            action_res = {'REDO': False}

            # save this run time
            with open(last_run_file, 'wb') as f:
                pickle.dump(now, f)
            #import epdb; epdb.st()



    def process(self, usecache=True):
        """Processes the Issue"""
        # clear all actions
        self.actions = {
            'newlabel': [],
            'unlabel':  [],
            'comments': [],
            'close': False,
        }

        # clear module maintainers
        self.module_maintainers = []

        # print some general info about the Issue to be processed
        print("\nIssue #%s: %s" % (self.issue.number,
                                (self.issue.instance.title).encode('ascii','ignore')))
        print("Created at %s" % self.issue.instance.created_at)
        print("Updated at %s" % self.issue.instance.updated_at)

        # get the template data
        self.template_data = self.issue.get_template_data()
        # was the issue type defined correctly?
        issue_type_defined = False
        issue_type_valid = False
        issue_type = False
        if 'issue type' in self.template_data:
            issue_type_defined = True
            issue_type = self.template_data['issue type']
            if issue_type.lower() in self.VALID_ISSUE_TYPES:
                issue_type_valid = True

        # was component specified?
        component_defined = 'component name' in self.template_data
        # extract the component
        component = self.template_data.get('component name', None)
        # save the real name
        self.match = self.module_indexer.find_match(component) or {}
        self.module = self.match.get('name', None)
        # check if component is a known module
        component_isvalid = self.module_indexer.is_valid(component)

        # smart match modules
        if not component_isvalid:
            smatch = self.smart_match_module()
            if self.module_indexer.is_valid(smatch):
                self.module = smatch
                component = smatch
                self.match = self.module_indexer.find_match(smatch)

        DF = DescriptionFixer(self.issue, self.module_indexer, self.match)
        self.issue.new_description = DF.new_description
        #import epdb; epdb.st()

        # filed under the correct repository?
        this_repo = False
        correct_repo = None
        # who maintains this?
        maintainers = []

        if not component_isvalid:
            pass
        else:
            correct_repo = self.module_indexer.\
                            get_repository_for_module(component)
            if correct_repo == self.github_repo:
                this_repo = True
                maintainers = self.get_module_maintainers()

        # Has the maintainer -ever- commented?
        maintainer_commented = False
        if component_isvalid:
            maintainer_commented = self.has_maintainer_commented()

        waiting_on_maintainer = False
        if component_isvalid:
            waiting_on_maintainer = self.is_waiting_on_maintainer()

        # How long ago did the maintainer last comment?
        maintainer_last_comment_age = -1
        if component_isvalid:
            maintainer_last_comment_age = self.age_of_last_maintainer_comment()


        ###########################################################
        #                   Enumerate Actions
        ###########################################################

        self.keep_current_main_labels()
        self.debug(msg='desired_comments: %s' % self.issue.desired_comments)

        '''
        self.add_desired_labels_by_issue_type()
        self.debug(msg='desired_comments: %s' % self.issue.desired_comments)

        self.add_desired_labels_by_ansible_version()
        self.debug(msg='desired_comments: %s' % self.issue.desired_comments)

        self.add_desired_labels_by_namespace()
        self.debug(msg='desired_comments: %s' % self.issue.desired_comments)
        '''

        '''
        self.add_desired_labels_by_maintainers()
        self.debug(msg='desired_comments: %s' % self.issue.desired_comments)
        '''

        #self.process_comments()
        self.process_history(usecache=usecache)
        self.debug(msg='desired_comments: %s' % self.issue.desired_comments)
        
        self.create_actions()
        self.debug(msg='desired_comments: %s' % self.issue.desired_comments)

        '''
        ###########################################################
        #                        LOG 
        ###########################################################
        print("Submitter: %s" % self.issue.get_submitter())
        print("Issue Type Defined: %s" % issue_type_defined)
        print("Issue Type Valid: %s" % issue_type_valid)
        print("Issue Type: %s" % issue_type)
        print("Component Defined: %s" % component_defined)
        print("Component Name: %s" % component)
        print("Component is Valid Module: %s" % component_isvalid)
        print("Component in this repo: %s" % this_repo)
        '''
        print("Module: %s" % self.module)
        if self.match:
            print("MModule: %s" % self.match['name'])
        else:
            print("MModule: %s" % self.match)
        print("Maintainer(s): %s" \
            % ', '.join(self.get_module_maintainers(expand=False)))
        print("Submitter: %s" % self.issue.get_submitter())
        print("Total Comments: %s" % len(self.issue.current_comments))
        for x in self.issue.current_comments:
            print("\t%s %s %s" % (x.created_at.isoformat(),
                        x.user.login,
                        [y for y in self.VALID_COMMANDS if y in x.body \
                        and not '!' + y in x.body] or ''))
        print("Current Labels: %s" % ', '.join(sorted(self.issue.current_labels)))
        '''
        #print("Maintainer(s) Have Commented: %s" % maintainer_commented)
        #print("Maintainer(s) Comment Age: %s days" % maintainer_last_comment_age)
        #print("Waiting on Maintainer(s): %s" % waiting_on_maintainer)
        print("Current Labels: %s" % ', '.join(sorted(self.issue.current_labels)))
        print("Desired Labels: %s" % ', '.join(sorted(self.issue.desired_labels)))
        print("Current Comments: %s" % len(self.issue.current_comments))
        print("Desired Comments: %s" % ', '.join(self.issue.desired_comments))
        print("Actions: ...")
        print("CLOSE: %s" % self.actions['close'])
        print("NEWLABEL:")
        import pprint; pprint.pprint(self.actions['newlabel'])
        print("UNLABEL:")
        import pprint; pprint.pprint(self.actions['unlabel'])
        for comment in self.actions['comments']:
            print('ADD_COMMENT: %s' % comment[:80])
        '''
        import pprint; pprint.pprint(self.actions)

        # invoke the wizard
        action_meta = self.apply_actions()
        return action_meta


    def create_actions(self):
        """Create actions from the desired label/unlabel/comment actions"""

        if 'bot_broken' in self.issue.desired_labels:
            # If the bot is broken, do nothing other than set the broken label
            self.actions['comments'] = []
            self.actions['newlabel'] = []
            self.actions['unlabel'] = []
            self.actions['close'] = False
            if not 'bot_broken' in self.issue.current_labels:
                self.actions['newlabel'] = ['bot_broken']                
            return

        if self.issue.desired_state != self.issue.instance.state:
            if self.issue.desired_state == 'closed':
                # close the issue ...
                self.actions['close'] = True
                if 'issue_closure' in self.issue.desired_comments:
                    comment = self.render_comment(boilerplate='issue_closure')
                    self.actions['comments'].append(comment)
                return

        resolved_desired_labels = []
        for desired_label in self.issue.desired_labels:
            resolved_desired_label = self.issue.resolve_desired_labels(
                desired_label
            )
            if desired_label != resolved_desired_label:
                resolved_desired_labels.append(resolved_desired_label)
                if (resolved_desired_label not in self.issue.get_current_labels()):
                    self.issue.add_desired_comment(desired_label)
                    self.actions['newlabel'].append(resolved_desired_label)
            else:
                resolved_desired_labels.append(desired_label)
                if desired_label not in self.issue.get_current_labels():
                    self.actions['newlabel'].append(desired_label)
                    '''
                    if os.path.exists("templates/" + 'issue_' + desired_label + ".j2"):
                        self.issue.add_desired_comment('issue_' + desired_label)
                    '''

        for current_label in self.issue.get_current_labels():
            if current_label in self.IGNORE_LABELS:
                continue
            if current_label not in resolved_desired_labels:
                self.actions['unlabel'].append(current_label)

        # should only make one comment at a time
        if len(self.issue.desired_comments) > 1:
            if 'issue_wrong_repo' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_wrong_repo']
            elif 'issue_invalid_module' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_invalid_module']
            elif 'issue_module_no_maintainer' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_module_no_maintainer']
            elif 'issue_needs_info' in self.issue.desired_comments \
                and 'needs_info' in self.issue.desired_labels:
                self.issue.desired_comments = ['issue_needs_info']
            elif 'issue_needs_info' in self.issue.desired_comments \
                and 'needs_info' not in self.issue.desired_labels:
                self.issue.desired_comments.remove('issue_needs_info')
            else:
                import epdb; epdb.st()        

        # Do not comment needs_info if it's not a label
        if 'issue_needs_info' in self.issue.desired_comments \
            and not 'needs_info' in self.issue.desired_labels:
            self.issue.desired_comments.remove('issue_needs_info')    

        # render the comments
        for boilerplate in self.issue.desired_comments:
            comment = self.render_comment(boilerplate=boilerplate)
            self.debug(msg=boilerplate)
            self.actions['comments'].append(comment)

        # do not re-comment
        for idx,comment in enumerate(self.actions['comments']):
            if self.issue.current_comments:
                if self.issue.current_comments[-1].body == comment:
                    self.debug(msg="Removing repeat comment from actions")
                    self.actions['comments'].remove(comment)
        #import epdb; epdb.st()


    def process_history(self, usecache=True):
        self.meta = {}
        today = self.get_current_time()

        # Build the history
        self.debug(msg="Building event history ...")
        self.history = HistoryWrapper(self.issue, usecache=usecache)

        # what was the last commment?
        bot_broken = False
        if self.issue.current_comments:
            for comment in self.issue.current_comments:
                if 'bot_broken' in comment.body:
                    bot_broken = True

        # who made this and when did they last comment?
        submitter = self.issue.get_submitter()
        submitter_last_commented = self.history.last_commented_at(submitter)
        submitter_last_comment = self.history.last_comment(submitter)
        submitter_last_notified = self.history.last_notified(submitter)

        # what did they not provide?
        missing_sections = self.issue.get_missing_sections()
        if 'ansible version' in missing_sections:
            missing_sections.remove('ansible version')

        # DEBUG + FIXME - speeds up bulk triage
        if 'component name' in missing_sections and self.match:
            missing_sections.remove('component name')
        #import epdb; epdb.st()

        # Is this a valid module?
        valid_module = False
        correct_repo = True
        if self.match:
            valid_module = True
            if self.match['repository'] != self.github_repo:
                correct_repo = False
            #import epdb; epdb.st()

        # Do these after evaluating the module
        self.add_desired_labels_by_issue_type()
        if 'ansible version' in missing_sections:
            #self.debug(msg='desired_comments: %s' % self.issue.desired_comments)
            self.add_desired_labels_by_ansible_version()
        #self.debug(msg='desired_comments: %s' % self.issue.desired_comments)
        self.add_desired_labels_by_namespace()
        #self.debug(msg='desired_comments: %s' % self.issue.desired_comments)


        # Who are the maintainers?
        maintainers = [x for x in self.get_module_maintainers()]
        #if 'ansible' in maintainers:
        #    maintainers.remove('ansible')
        #    maintainers += self.ansible_members

        #print("MAINTAINERS: %s" % maintainers)
        if 'ansible' in maintainers:
            maintainers.remove('ansible')
        maintainers.extend(self.ansible_members)
        if 'ansibot' in maintainers:
            maintainers.remove('ansibot')
        if submitter in maintainers:
            maintainers.remove(submitter)
        maintainers = sorted(set(maintainers))

        # Has maintainer been notified? When?
        notification_maintainers = self.get_module_maintainers()
        if 'ansible' in notification_maintainers:
            notification_maintainers.extend(self.ansible_members)
        if 'ansibot' in notification_maintainers:
            notification_maintainers.remove('ansibot')
        maintainer_last_notified = self.history.\
                    last_notified(notification_maintainers)
        #import epdb; epdb.st()

        # Has maintainer viewed issue?
        maintainer_viewed = self.history.has_viewed(maintainers)
        maintainer_last_viewed = self.history.last_viewed_at(maintainers)
        #import epdb; epdb.st()

        # Has maintainer been mentioned?
        maintainer_mentioned = self.history.is_mentioned(maintainers)

        # Has maintainer viewed issue?
        maintainer_viewed = self.history.has_viewed(maintainers)

        # Has the maintainer ever responded?
        maintainer_commented = self.history.has_commented(maintainers)
        maintainer_last_commented = self.history.last_commented_at(maintainers)
        maintainer_last_comment = self.history.last_comment(maintainers)
        maintainer_comments = self.history.get_user_comments(maintainers)
        #import epdb; epdb.st()

        # Was the maintainer the last commentor?
        last_commentor_ismaintainer = False
        last_commentor_issubmitter = False
        last_commentor = self.history.last_commentor()
        if last_commentor in maintainers and last_commentor != self.github_user:
            last_commentor_ismaintainer = True
        elif last_commentor == submitter:
            last_commentor_issubmitter = True

        # Did the maintainer issue a command?
        maintainer_commands = self.history.get_commands(maintainers, 
                                                        self.VALID_COMMANDS)
        #import epdb; epdb.st()
        maintainer_command_close = False
        maintainer_command_needsinfo = False
        maintainer_command_not_needsinfo = False
        maintainer_command_notabug = False
        maintainer_command_wontfix = False
        maintainer_command_resolved_bug = False
        maintainer_command_resolved_pr = False
        maintainer_command_needscontributor = False
        maintainer_command_duplicateof = False
        if maintainer_commented and not maintainer_last_comment:
            print('ERROR: should have a comment from maintainer')
            import epdb; epdb.st()
        elif maintainer_last_comment and last_commentor_ismaintainer:
            maintainer_last_comment = maintainer_last_comment.strip()            
            if 'needs_info' in maintainer_last_comment \
                and not '!needs_info' in maintainer_last_comment:
                maintainer_command_needsinfo = True
            elif '!needs_info' in maintainer_last_comment:
                maintainer_command_not_needsinfo = True
            elif 'notabug' in maintainer_last_comment:
                maintainer_command_notabug = True
                maintainer_command_close = True
            elif 'wontfix' in maintainer_last_comment:
                maintainer_command_wontfix = True
                maintainer_command_close = True
            elif 'bug_resolved' in maintainer_last_comment:
                maintainer_command_resolved_bug = True
                maintainer_command_close = True
            elif 'resolved_by_pr' in maintainer_last_comment:
                maintainer_command_resolved_pr = True
                maintainer_command_close = True
            elif 'needs_contributor' in maintainer_last_comment:
                maintainer_command_needscontributor = True
            elif 'duplicate_of' in maintainer_last_comment:
                maintainer_command_duplicateof = True
                maintainer_command_close = True
        elif maintainer_commands:
            # are there any persistant commands?
            if 'needs_contributor' in maintainer_commands:
                maintainer_command_needscontributor = True
            elif not missing_sections and not submitter_last_commented and maintainer_commands[-1] == 'needs_info':
                maintainer_command_needsinfo = True
            elif not missing_sections and not submitter_last_commented and maintainer_commands[-1] == '!needs_info':
                maintainer_command_not_needsinfo = True
            #import epdb; epdb.st()
        #import epdb; epdb.st()

        # Has the maintainer ever subscribed?
        maintainer_subscribed = self.history.has_subscribed(maintainers)

        # Was it ever needs_info?
        was_needs_info = self.history.was_labeled(label='needs_info')
        needsinfo_last_applied = self.history.label_last_applied('needs_info')
        needsinfo_last_removed = self.history.label_last_removed('needs_info')
        #import epdb; epdb.st()

        # Still needs_info?
        needsinfo_add = False
        needsinfo_remove = False
        if 'needs_info' in self.issue.current_labels:
            if submitter_last_commented and needsinfo_last_applied:
                if submitter_last_commented > needsinfo_last_applied \
                    and not missing_sections:
                    needsinfo_remove = True
        if maintainer_command_needsinfo and maintainer_last_commented:
            if submitter_last_commented and maintainer_last_commented:
                if submitter_last_commented > maintainer_last_commented:
                    needsinfo_add = False
                    needsinfo_remove = True
            else:
                needsinfo_add = True
                needsinfo_remove = False

        # Is needs_info stale or expired?
        needsinfo_age = None
        needsinfo_stale = False
        needsinfo_expired = False
        if 'needs_info' in self.issue.current_labels: 
            time_delta = today - needsinfo_last_applied
            needsinfo_age = time_delta.days
            if needsinfo_age > 14:
                needsinfo_stale = True
            if needsinfo_age > 56:
                needsinfo_expired = True

        # Should we be in waiting_on_maintainer mode?
        maintainer_waiting_on = False
        if (needsinfo_remove or not needsinfo_add) \
            or not was_needs_info \
            and not missing_sections:
            maintainer_waiting_on = True

        # Should we [re]notify the submitter?
        submitter_waiting_on = False
        submitter_to_ping = False
        submitter_to_reping = False
        if not maintainer_waiting_on:
            submitter_waiting_on = True

        if missing_sections:
            submitter_waiting_on = True
            maintainer_waiting_on = False
        else:
            if 'needs_info' in self.issue.current_labels \
                and not maintainer_command_not_needsinfo:
                needsinfo_remove = True                
                submitter_waiting_on = False
                maintainer_waiting_on = True

        if maintainer_command_not_needsinfo:
            submitter_waiting_on = False
            maintainer_waiting_on = True

        if maintainer_command_needsinfo:
            submitter_waiting_on = True
            maintainer_waiting_on = False
            needsinfo_add = True
            needsinfo_remove = False

        # Time to [re]ping maintainer?
        maintainer_to_ping = False
        maintainer_to_reping = False
        if maintainer_waiting_on:
            #import epdb; epdb.st()
            if maintainer_viewed and not maintainer_last_notified:
                time_delta = today - maintainer_last_viewed
                view_age = time_delta.days
                if view_age > 14:
                    maintainer_to_reping = True
            elif maintainer_last_notified:
                time_delta = today - maintainer_last_notified
                ping_age = time_delta.days
                if ping_age > 14:
                    maintainer_to_reping = True
            else:
                maintainer_to_ping = True

        # Time to [re]ping the submitter?
        if submitter_waiting_on:
            if submitter_last_notified:
                time_delta = today - submitter_last_notified
                notification_age = time_delta.days
                if notification_age > 14:
                    submitter_to_reping = True
                else:
                    submitter_to_reping = False
                submitter_to_ping = False
            else:
                submitter_to_ping = True
                submitter_to_reping = False

        issue_type = self.template_data.get('issue type', None)
        issue_type = self.issue_type_to_label(issue_type)
        self.meta['issue_type'] = issue_type

        # new module requests need to disable everything
        #   https://github.com/ansible/ansible-modules-core/issues/4112
        #   https://github.com/ansible/ansible-modules-core/issues/2267
        #   https://github.com/ansible/ansible-modules-core/issues/2626
        #   https://github.com/ansible/ansible-modules-core/issues/645
        new_module_request = False
        if 'feature_idea' in self.issue.desired_labels:
            if self.template_data['component name'] == 'new':
                new_module_request = True

        # reset the maintainers
        maintainers = self.get_module_maintainers()

        #################################################
        # FINAL LOGIC LOOP
        #################################################

        if bot_broken:
            self.debug(msg='broken bot stanza')
            self.issue.add_desired_label('bot_broken')

        elif maintainer_command_close:

            self.debug(msg='maintainer closure stanza')

            # Need to close the issue ...
            self.issue.set_desired_state('closed')


        elif new_module_request:

            self.debug(msg='new module request stanza')
            self.issue.desired_comments = []
            for label in self.issue.current_labels:
                if not label in self.issue.desired_labels:
                    self.issue.desired_labels.append(label)

        elif not correct_repo:

            self.debug(msg='wrong repo stanza')
            self.issue.desired_comments = ['issue_wrong_repo']
            self.actions['close'] = True
            #import epdb; epdb.st()

        elif not valid_module and not maintainer_command_needsinfo:
            self.debug(msg='invalid module stanza')

            #import epdb; epdb.st()
            self.issue.add_desired_label('needs_info')
            if 'issue_invalid_module' not in self.issue.current_bot_comments \
                and not 'issue_needs_info' in self.issue.current_bot_comments:
                self.issue.desired_comments = ['issue_invalid_module']

        elif not maintainers and not maintainer_command_needsinfo:

            self.debug(msg='no maintainer stanza')

            self.issue.add_desired_label('waiting_on_maintainer')
            self.issue.add_desired_comment("issue_module_no_maintainer")

        elif maintainer_command_needscontributor:

            # maintainer can't or won't fix this, but would like someone else to
            self.debug(msg='maintainer needs contributor stanza')
            self.issue.add_desired_label('waiting_on_contributor')
            #import epdb; epdb.st()            

        elif maintainer_waiting_on:

            self.debug(msg='maintainer wait stanza')
            #import epdb; epdb.st()

            self.issue.add_desired_label('waiting_on_maintainer')
            if len(self.issue.current_comments) == 0:
                if issue_type:
                    self.issue.add_desired_comment('issue_new')
            else:
                if maintainers != ['DEPRECATED']:
                    if maintainer_to_ping and maintainers:
                        self.issue.add_desired_comment("issue_notify_maintainer")
                    elif maintainer_to_reping and maintainers:
                        self.issue.add_desired_comment("issue_renotify_maintainer")
                #import epdb; epdb.st()

        elif submitter_waiting_on:

            self.debug(msg='submitter wait stanza')
            #import epdb; epdb.st()

            if 'waiting_on_maintainer' in self.issue.desired_labels:
                self.issue.desired_labels.remove('waiting_on_maintainer')

            if (needsinfo_add or missing_sections) \
                or (not needsinfo_remove and missing_sections) \
                or (needsinfo_add and not missing_sections): 

                self.issue.add_desired_label('needs_info')
                if len(self.issue.current_comments) == 0:
                    self.issue.add_desired_comment("issue_needs_info")

                # needs_info: warn if stale, close if expired
                elif needsinfo_expired:
                    self.issue.add_desired_comment("issue_closure")
                    self.issue.set_desired_state('closed')
                elif needsinfo_stale \
                    and (submitter_to_ping or submitter_to_reping):
                    self.issue.add_desired_comment("issue_pending_closure")
Exemple #2
0
class TriageIssues(DefaultTriager):

    VALID_COMMANDS = ['needs_info', '!needs_info', 'notabug',
                      'bot_broken', 'bot_skip',
                      'wontfix', 'bug_resolved', 'resolved_by_pr',
                      'needs_contributor', 'duplicate_of']

    CLOSURE_COMMANDS = [
        'notabug',
        'wontfix',
        'bug_resolved',
        'resolved_by_pr',
        'duplicate_of'
    ]

    # re-notify interval for maintainers
    RENOTIFY_INTERVAL = 14

    # max limit for needs_info notifications
    RENOTIFY_EXPIRE = 56

    # re-notify interval by this number for features
    FEATURE_RENOTIFY_INTERVAL = 60

    # the max comments per week before ansibot becomes a "spammer"
    MAX_BOT_COMMENTS_PER_WEEK = 5

    def run(self, useapiwrapper=True):
        """Starts a triage run"""

        # how many issues have been processed
        self.icount = 0

        # Create the api connection
        if not useapiwrapper:
            # use the default non-caching connection
            self.repo = self._connect().get_repo(self._get_repo_path())
        else:
            # make use of the special caching wrapper for the api
            self.gh = self._connect()
            self.ghw = GithubWrapper(self.gh)
            self.repo = self.ghw.get_repo(self._get_repo_path())

        # extend the ignored labels by repo
        if hasattr(self, 'IGNORE_LABELS_ADD'):
            self.IGNORE_LABELS.extend(self.IGNORE_LABELS_ADD)

        if self.number:
            issue = self.repo.get_issue(int(self.number))
            self.issue = IssueWrapper(repo=self.repo, issue=issue, cachedir=self.cachedir)
            self.issue.get_events()
            self.issue.get_comments()
            self.process()
        else:

            last_run = None
            now = self.get_current_time()
            last_run_file = '~/.ansibullbot/cache'
            if self.github_repo == 'ansible':
                last_run_file += '/ansible/ansible/'
            else:
                last_run_file += '/ansible/ansible-modules-%s/' % self.github_repo
            last_run_file += 'issues/last_run.pickle'
            last_run_file = os.path.expanduser(last_run_file)
            if os.path.isfile(last_run_file):
                try:
                    with open(last_run_file, 'rb') as f:
                        last_run = pickle.load(f)
                except Exception as e:
                    print(e)

            if last_run and not self.no_since:
                self.debug('Getting issues updated/created since %s' % last_run)
                issues = self.repo.get_issues(since=last_run)
            else:
                self.debug('Getting ALL issues')
                issues = self.repo.get_issues()

            for issue in issues:
                self.icount += 1
                if self.start_at and issue.number > self.start_at:
                    continue
                if self.is_pr(issue):
                    continue
                self.issue = IssueWrapper(repo=self.repo, issue=issue, cachedir=self.cachedir)
                self.issue.get_events()
                self.issue.get_comments()
                action_res = self.process()
                if action_res:
                    while action_res['REDO']:
                        issue = self.repo.get_issue(int(issue.number))
                        self.issue = IssueWrapper(repo=self.repo, issue=issue, cachedir=self.cachedir)
                        self.issue.get_events()
                        self.issue.get_comments()
                        action_res = self.process()
                        if not action_res:
                            action_res = {'REDO': False}

            # save this run time
            with open(last_run_file, 'wb') as f:
                pickle.dump(now, f)

    @RateLimited
    def process(self, usecache=True):
        """Processes the Issue"""

        # basic processing
        self._process()

        # who maintains this?
        maintainers = []

        if self.meta.get('component_valid', False):
            correct_repo = self.match.get('repository')
            if correct_repo != self.github_repo:
                self.meta['correct_repo'] = False
            else:
                self.meta['correct_repo'] = True
                maintainers = self.get_module_maintainers()
                if not maintainers:
                    #issue_module_no_maintainer
                    #import epdb; epdb.st()
                    pass


        ###########################################################
        #                   Enumerate Actions
        ###########################################################

        self.keep_current_main_labels()
        self.debug(msg='desired_comments: %s' % self.issue.desired_comments)

        self.process_history(usecache=usecache)
        self.debug(msg='desired_comments: %s' % self.issue.desired_comments)

        self.create_actions()
        self.debug(msg='desired_comments: %s' % self.issue.desired_comments)

        print("Module: %s" % self.module)
        if self.match:
            print("MModule: %s" % self.match['name'])
        else:
            print("MModule: %s" % self.match)
        print("Maintainer(s): %s" \
            % ', '.join(self.get_module_maintainers(expand=False)))
        print("Submitter: %s" % self.issue.get_submitter())
        print("Total Comments: %s" % len(self.issue.current_comments))
        self.print_comment_list()
        print("Current Labels: %s" % ', '.join(sorted(self.issue.current_labels)))

        # invoke the wizard
        import pprint; pprint.pprint(self.actions)
        action_meta = self.apply_actions()
        return action_meta


    def create_actions(self):
        """Create actions from the desired label/unlabel/comment actions"""

        # do nothing for bot_skip
        if self.meta['bot_skip']:
            return

        if 'bot_broken' in self.issue.desired_labels:
            # If the bot is broken, do nothing other than set the broken label
            self.actions['comments'] = []
            self.actions['newlabel'] = []
            self.actions['unlabel'] = []
            self.actions['close'] = False
            if not 'bot_broken' in self.issue.current_labels:
                self.actions['newlabel'] = ['bot_broken']
            return

        # add the version labels
        self.create_label_version_actions()

        if self.issue.desired_state != self.issue.instance.state:
            if self.issue.desired_state == 'closed':
                # close the issue ...
                self.actions['close'] = True

                # We only want up to 1 comment when an issue is closed
                if 'issue_closure' in self.issue.desired_comments:
                    self.issue.desired_comments = ['issue_closure']
                elif 'issue_deprecated_module' in self.issue.desired_comments:
                    self.issue.desired_comments = ['issue_deprecated_module']
                else:
                    self.issue.desired_comments = []

                if self.issue.desired_comments:
                    comment = self.render_comment(
                                boilerplate=self.issue.desired_comments[0]
                              )
                    self.actions['comments'].append(comment)
                return

        resolved_desired_labels = []
        for desired_label in self.issue.desired_labels:
            resolved_desired_label = self.issue.resolve_desired_labels(
                desired_label
            )
            if desired_label != resolved_desired_label:
                resolved_desired_labels.append(resolved_desired_label)
                if (resolved_desired_label not in self.issue.get_current_labels()):
                    self.issue.add_desired_comment(desired_label)
                    self.actions['newlabel'].append(resolved_desired_label)
            else:
                resolved_desired_labels.append(desired_label)
                if desired_label not in self.issue.get_current_labels():
                    self.actions['newlabel'].append(desired_label)
                    '''
                    if os.path.exists("templates/" + 'issue_' + desired_label + ".j2"):
                        self.issue.add_desired_comment('issue_' + desired_label)
                    '''

        for current_label in self.issue.get_current_labels():
            if current_label in self.IGNORE_LABELS:
                continue
            if current_label not in resolved_desired_labels:
                self.actions['unlabel'].append(current_label)

        # should only make one comment at a time
        if len(self.issue.desired_comments) > 1:
            if 'issue_wrong_repo' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_wrong_repo']
            elif 'issue_invalid_module' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_invalid_module']
            elif 'issue_module_no_maintainer' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_module_no_maintainer']
            elif 'issue_needs_info' in self.issue.desired_comments \
                and 'needs_info' in self.issue.desired_labels:
                self.issue.desired_comments = ['issue_needs_info']
            elif 'issue_needs_info' in self.issue.desired_comments \
                and 'needs_info' not in self.issue.desired_labels:
                self.issue.desired_comments.remove('issue_needs_info')
            else:
                import epdb; epdb.st()

        # Do not comment needs_info if it's not a label
        if 'issue_needs_info' in self.issue.desired_comments \
            and not 'needs_info' in self.issue.desired_labels:
            self.issue.desired_comments.remove('issue_needs_info')

        # render the comments
        for boilerplate in self.issue.desired_comments:
            comment = self.render_comment(boilerplate=boilerplate)
            self.debug(msg=boilerplate)
            self.actions['comments'].append(comment)

        # do not re-comment
        for idx,comment in enumerate(self.actions['comments']):
            if self.issue.current_comments:
                if self.issue.current_comments[-1].body == comment:
                    self.debug(msg="Removing repeat comment from actions")
                    self.actions['comments'].remove(comment)
        #import epdb; epdb.st()


    def create_label_version_actions(self):
        if not self.ansible_label_version:
            return
        expected = 'affects_%s' % self.ansible_label_version
        if expected not in self.valid_labels:
            print("NEED NEW LABEL: %s" % expected)
            import epdb; epdb.st()

        candidates = [x for x in self.issue.current_labels]
        candidates = [x for x in candidates if x.startswith('affects_')]
        candidates = [x for x in candidates if x != expected]
        if len(candidates) > 0:
            #for cand in candidates:
            #    self.issue.pop_desired_label(name=cand)
            #return
            pass
        else:
            if expected not in self.issue.current_labels \
                or expected not in self.issue.desired_labels:
                self.issue.add_desired_label(name=expected)
        #import epdb; epdb.st()


    def create_commment_actions(self):
        '''Render desired comment templates'''

        # should only make one comment at a time
        if len(self.issue.desired_comments) > 1:
            if 'issue_wrong_repo' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_wrong_repo']
            elif 'issue_invalid_module' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_invalid_module']
            elif 'issue_module_no_maintainer' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_module_no_maintainer']
            elif 'issue_needs_info' in self.issue.desired_comments \
                and 'needs_info' in self.issue.desired_labels:
                self.issue.desired_comments = ['issue_needs_info']
            elif 'issue_needs_info' in self.issue.desired_comments \
                and 'needs_info' not in self.issue.desired_labels:
                self.issue.desired_comments.remove('issue_needs_info')
            else:
                import epdb; epdb.st()

        # Do not comment needs_info if it's not a label
        if 'issue_needs_info' in self.issue.desired_comments \
            and not 'needs_info' in self.issue.desired_labels:
            self.issue.desired_comments.remove('issue_needs_info')

        # render the comments
        for boilerplate in self.issue.desired_comments:
            comment = self.render_comment(boilerplate=boilerplate)
            self.debug(msg=boilerplate)
            self.actions['comments'].append(comment)

        # do not re-comment
        for idx,comment in enumerate(self.actions['comments']):
            if self.issue.current_comments:
                if self.issue.current_comments[-1].body == comment:
                    self.debug(msg="Removing repeat comment from actions")
                    self.actions['comments'].remove(comment)


    def create_label_actions(self):
        """Create actions from the desired label/unlabel/comment actions"""

        if 'bot_broken' in self.issue.desired_labels:
            # If the bot is broken, do nothing other than set the broken label
            self.actions['comments'] = []
            self.actions['newlabel'] = []
            self.actions['unlabel'] = []
            self.actions['close'] = False
            if not 'bot_broken' in self.issue.current_labels:
                self.actions['newlabel'] = ['bot_broken']
            return

        if self.meta['bot_skip']:
            return

        resolved_desired_labels = []
        for desired_label in self.issue.desired_labels:
            resolved_desired_label = self.issue.resolve_desired_labels(
                desired_label
            )
            if desired_label != resolved_desired_label:
                resolved_desired_labels.append(resolved_desired_label)
                if (resolved_desired_label not in self.issue.get_current_labels()):
                    self.issue.add_desired_comment(desired_label)
                    self.actions['newlabel'].append(resolved_desired_label)
            else:
                resolved_desired_labels.append(desired_label)
                if desired_label not in self.issue.get_current_labels():
                    self.actions['newlabel'].append(desired_label)

        for current_label in self.issue.get_current_labels():
            if current_label in self.IGNORE_LABELS:
                continue
            if current_label not in resolved_desired_labels:
                self.actions['unlabel'].append(current_label)



    def process_history(self, usecache=True):
        '''Steps through all known meta about the issue and decides what to do'''

        self.meta.update(self.get_facts())
        self.add_desired_labels_by_issue_type()
        self.add_desired_labels_by_ansible_version()
        self.add_desired_labels_by_namespace()

        #################################################
        # FINAL LOGIC LOOP
        #################################################

        if self.meta['bot_broken']:
            self.debug(msg='broken bot stanza')
            self.issue.add_desired_label('bot_broken')

        elif self.meta['bot_skip']:

            self.debug(msg='bot skip stanza')

            # clear out all actions and do nothing
            for k,v in six.iteritems(self.actions):
                if type(v) == list:
                    self.actions[k] = []
            self.actions['close'] = False

        elif self.meta['bot_spam']:

            self.debug(msg='bot spam stanza')

            # clear out all actions and do nothing
            for k,v in six.iteritems(self.actions):
                if type(v) == list:
                    self.actions[k] = []
            self.actions['close'] = False

            # do we mark this somehow?
            self.issue.add_desired_label('bot_broken')


        elif self.match and self.match.get('deprecated', False) \
            and 'feature_idea' in self.issue.desired_labels:

            self.debug(msg='deprecated module stanza')

            # Make the deprecated comment
            self.issue.desired_comments = ['issue_deprecated_module']

            # Close the issue ...
            self.issue.set_desired_state('closed')

        elif not self.meta['maintainers_known'] and self.meta['valid_module']:

            self.debug(msg='unknown maintainer stanza')
            self.issue.desired_comments = ['issue_module_no_maintainer']

        elif self.meta['maintainer_closure']:

            self.debug(msg='maintainer closure stanza')

            # Need to close the issue ...
            self.issue.set_desired_state('closed')

        elif self.meta['new_module_request']:

            self.debug(msg='new module request stanza')
            self.issue.desired_comments = []
            for label in self.issue.current_labels:
                if not label in self.issue.desired_labels:
                    self.issue.desired_labels.append(label)

        elif not self.meta['correct_repo']:

            self.debug(msg='wrong repo stanza')
            self.issue.desired_comments = ['issue_wrong_repo']
            self.actions['close'] = True

        elif not self.meta['valid_module'] and \
            not self.meta['maintainer_command_needsinfo']:

            self.debug(msg='invalid module stanza')

            self.issue.add_desired_label('needs_info')
            if 'issue_invalid_module' not in self.issue.current_bot_comments \
                and not 'issue_needs_info' in self.issue.current_bot_comments:
                self.issue.desired_comments = ['issue_invalid_module']

        elif not self.meta['notification_maintainers'] and \
            not self.meta['maintainer_command_needsinfo']:

            self.debug(msg='no maintainer stanza')

            self.issue.add_desired_label('waiting_on_maintainer')
            self.issue.add_desired_comment("issue_module_no_maintainer")

        elif self.meta['maintainer_command'] == 'needs_contributor':

            # maintainer can't or won't fix this, but would like someone else to
            self.debug(msg='maintainer needs contributor stanza')
            self.issue.add_desired_label('waiting_on_contributor')

        elif self.meta['maintainer_waiting_on']:

            self.debug(msg='maintainer wait stanza')

            self.issue.add_desired_label('waiting_on_maintainer')
            if len(self.issue.current_comments) == 0:
                # new issue
                if self.meta['issue_type']:
                    if self.meta['submitter'] not in self.meta['notification_maintainers']:
                        # ping the maintainer
                        self.issue.add_desired_comment('issue_new')
                    else:
                        # do not send intial ping to maintainer if also submitter
                        if 'issue_new' in self.issue.desired_comments:
                            self.issue.desired_comments.remove('issue_new')
            else:
                # old issue -- renotify
                if not self.match['deprecated'] and self.meta['notification_maintainers']:
                    if self.meta['maintainer_to_ping']:
                        self.issue.add_desired_comment("issue_notify_maintainer")
                    elif self.meta['maintainer_to_reping']:
                        self.issue.add_desired_comment("issue_renotify_maintainer")

        elif self.meta['submitter_waiting_on']:

            self.debug(msg='submitter wait stanza')

            if 'waiting_on_maintainer' in self.issue.desired_labels:
                self.issue.desired_labels.remove('waiting_on_maintainer')

            if (self.meta['needsinfo_add'] or self.meta['missing_sections']) \
                or (not self.meta['needsinfo_remove'] and self.meta['missing_sections']) \
                or (self.meta['needsinfo_add'] and not self.meta['missing_sections']):


                #import epdb; epdb.st()
                self.issue.add_desired_label('needs_info')
                if len(self.issue.current_comments) == 0 or \
                    not self.meta['maintainer_commented']:

                    if self.issue.current_bot_comments:
                        if 'issue_needs_info' not in self.issue.current_bot_comments:
                            self.issue.add_desired_comment("issue_needs_info")
                    else:
                        self.issue.add_desired_comment("issue_needs_info")

                # needs_info: warn if stale, close if expired
                elif self.meta['needsinfo_expired']:
                    self.issue.add_desired_comment("issue_closure")
                    self.issue.set_desired_state('closed')
                elif self.meta['needsinfo_stale'] \
                    and (self.meta['submitter_to_ping'] or self.meta['submitter_to_reping']):
                    self.issue.add_desired_comment("issue_pending_closure")


    def get_history_facts(self, usecache=True):
        return self.get_facts(usecache=usecache)

    def get_facts(self, usecache=True):
        '''Only used by the ansible/ansible triager at the moment'''
        hfacts = {}
        today = self.get_current_time()

        self.history = HistoryWrapper(
                        self.issue,
                        usecache=usecache,
                        cachedir=self.cachedir
                       )

        # what was the last commment?
        bot_broken = False
        if self.issue.current_comments:
            for comment in self.issue.current_comments:
                if 'bot_broken' in comment.body:
                    bot_broken = True

        # did someone from ansible want this issue skipped?
        bot_skip = False
        if self.issue.current_comments:
            for comment in self.issue.current_comments:
                if 'bot_skip' in comment.body and comment.user.login in self.ansible_members:
                    bot_skip = True

        #########################################################
        # Facts for new "triage" workflow
        #########################################################

        # these logins don't count for the next set of facts
        exclude_logins = ['ansibot', 'gregdek', self.issue.instance.user.login]

        # Has a human (not ansibot + not submitter) ever changed labels?
        lchanges = [x for x in self.history.history if x['event'] in ['labeled', 'unlabeled'] and x['actor'] not in exclude_logins]
        if lchanges:
            hfacts['human_labeled'] = True
        else:
            hfacts['human_labeled'] = False

        # Has a human (not ansibot + not submitter) ever changed the title?
        tchanges = [x for x in self.history.history if x['event'] in ['renamed'] and x['actor'] not in exclude_logins]
        if tchanges:
            hfacts['human_retitled'] = True
        else:
            hfacts['human_retitled'] = False

        # Has a human (not ansibot + not submitter) ever changed the description?
        # ... github doesn't record an event for this. Lame.

        # Has a human (not ansibot + not submitter) ever set a milestone?
        mchanges = [x for x in self.history.history if x['event'] in ['milestoned', 'demilestoned'] and x['actor'] not in exclude_logins]
        if mchanges:
            hfacts['human_milestoned'] = True
        else:
            hfacts['human_milestoned'] = False

        # Has a human (not ansibot + not submitter) ever set/unset assignees?
        achanges = [x for x in self.history.history if x['event'] in ['assigned', 'unassigned'] and x['actor'] not in exclude_logins]
        if achanges:
            hfacts['human_assigned'] = True
        else:
            hfacts['human_assigned'] = False

        #########################################################

        # Has the bot been overzealous with comments?
        hfacts['bot_spam'] = False
        bcg = self.history.get_user_comments_groupby('ansibot', groupby='w')
        for k,v in six.iteritems(bcg):
            if v >= self.MAX_BOT_COMMENTS_PER_WEEK:
                hfacts['bot_spam'] = True

        # Is this a new module?
        hfacts['new_module_request'] = False
        if 'feature_idea' in self.issue.desired_labels:
            if self.template_data['component name'] == 'new':
                hfacts['new_module_request'] = True

        # who made this and when did they last comment?
        submitter = self.issue.get_submitter()
        submitter_last_commented = self.history.last_commented_at(submitter)
        if not submitter_last_commented:
            submitter_last_commented = self.issue.instance.created_at
            #import epdb; epdb.st()
        submitter_last_comment = self.history.last_comment(submitter)
        submitter_last_notified = self.history.last_notified(submitter)

        # what did they not provide?
        missing_sections = self.issue.get_missing_sections()

        # Is this a valid module?
        if self.match:
            self.meta['valid_module'] = True
        else:
            self.meta['valid_module'] = False

        # Filed in the right place?
        if self.meta['valid_module']:
            if self.match['repository'] != self.github_repo:
                hfacts['correct_repo'] = False
            else:
                hfacts['correct_repo'] = True
        else:
            hfacts['correct_repo'] = True

        # DEBUG + FIXME - speeds up bulk triage
        if 'component name' in missing_sections \
            and (self.match or self.github_repo == 'ansible'):
            missing_sections.remove('component name')
        #import epdb; epdb.st()

        # Who are the maintainers?
        maintainers = [x for x in self.get_module_maintainers()]
        #hfacts['maintainers'] = maintainers
        #import epdb; epdb.st()

        # Set a fact to indicate that we know the maintainer
        self.meta['maintainers_known'] = False
        if maintainers:
            self.meta['maintainers_known'] = True

        if 'ansible' in maintainers:
            maintainers.remove('ansible')
        maintainers.extend(self.ansible_members)
        if 'ansibot' in maintainers:
            maintainers.remove('ansibot')
        if submitter in maintainers:
            maintainers.remove(submitter)
        maintainers = sorted(set(maintainers))

        # Has maintainer been notified? When?
        notification_maintainers = [x for x in self.get_module_maintainers()]
        if 'ansible' in notification_maintainers:
            notification_maintainers.extend(self.ansible_members)
        if 'ansibot' in notification_maintainers:
            notification_maintainers.remove('ansibot')
        hfacts['notification_maintainers'] = notification_maintainers
        maintainer_last_notified = self.history.\
                    last_notified(notification_maintainers)

        # Has maintainer viewed issue?
        maintainer_viewed = self.history.has_viewed(maintainers)
        maintainer_last_viewed = self.history.last_viewed_at(maintainers)
        #import epdb; epdb.st()

        # Has maintainer been mentioned?
        maintainer_mentioned = self.history.is_mentioned(maintainers)

        # Has maintainer viewed issue?
        maintainer_viewed = self.history.has_viewed(maintainers)

        # Has the maintainer ever responded?
        maintainer_commented = self.history.has_commented(maintainers)
        maintainer_last_commented = self.history.last_commented_at(maintainers)
        maintainer_last_comment = self.history.last_comment(maintainers)
        maintainer_comments = self.history.get_user_comments(maintainers)
        #import epdb; epdb.st()

        # Was the maintainer the last commentor?
        last_commentor_ismaintainer = False
        last_commentor_issubmitter = False
        last_commentor = self.history.last_commentor()
        if last_commentor in maintainers and last_commentor != self.github_user:
            last_commentor_ismaintainer = True
        elif last_commentor == submitter:
            last_commentor_issubmitter = True

        # Did the maintainer issue a command?
        maintainer_commands = self.history.get_commands(maintainers,
                                                        self.VALID_COMMANDS)

        # Keep all commands
        hfacts['maintainer_commands'] = maintainer_commands
        # Set a bit for the last command given
        if hfacts['maintainer_commands']:
            hfacts['maintainer_command'] = hfacts['maintainer_commands'][-1]
        else:
            hfacts['maintainer_command'] = None

        # Is the last command a closure command?
        if hfacts['maintainer_command'] in self.CLOSURE_COMMANDS:
            hfacts['maintainer_closure'] = True
        else:
            hfacts['maintainer_closure'] = False

        # handle resolved_by_pr ...
        if 'resolved_by_pr' in maintainer_commands:
            maintainer_comments = self.history.get_user_comments(maintainers)
            maintainer_comments = [x for x in reversed(maintainer_comments) \
                                   if 'resolved_by_pr' in x]
            for comment in maintainer_comments:
                pr_number = extract_pr_number_from_comment(comment)
                hfacts['resolved_by_pr'] = {
                    'number': pr_number,
                    'merged': self.is_pr_merged(pr_number),
                }
                if not hfacts['resolved_by_pr']['merged']:
                    hfacts['maintainer_closure'] = False
                break

        # needs_info toggles
        ni_commands = [x for x in maintainer_commands if 'needs_info' in x]

        # Has the maintainer ever subscribed?
        maintainer_subscribed = self.history.has_subscribed(maintainers)

        # Was it ever needs_info?
        was_needs_info = self.history.was_labeled(label='needs_info')
        needsinfo_last_applied = self.history.label_last_applied('needs_info')
        needsinfo_last_removed = self.history.label_last_removed('needs_info')

        # Still needs_info?
        needsinfo_add = False
        needsinfo_remove = False

        if 'needs_info' in self.issue.current_labels:
            if not needsinfo_last_applied or not submitter_last_commented:
                import epdb; epdb.st()
            if submitter_last_commented > needsinfo_last_applied:
                needsinfo_add = False
                needsinfo_remove = True

        #if 'needs_info' in maintainer_commands and maintainer_last_commented:
        if ni_commands and maintainer_last_commented:
            if ni_commands[-1] == 'needs_info':
                #import epdb; epdb.st()
                if submitter_last_commented and maintainer_last_commented:
                    if submitter_last_commented > maintainer_last_commented:
                        needsinfo_add = False
                        needsinfo_remove = True
                else:
                    needsinfo_add = True
                    needsinfo_remove = False
            else:
                needsinfo_add = False
                needsinfo_remove = True

        # Save existing needs_info if not time to remove ...
        if 'needs_info' in self.issue.current_labels \
            and not needsinfo_add \
            and not needsinfo_remove:
            needsinfo_add = True

        if ni_commands and maintainer_last_commented:
            if maintainer_last_commented > submitter_last_commented:
                if ni_commands[-1] == 'needs_info':
                    needsinfo_add = True
                    needsinfo_remove = False
                else:
                    needsinfo_add = False
                    needsinfo_remove = True
        #import epdb; epdb.st()

        # Is needs_info stale or expired?
        needsinfo_age = None
        needsinfo_stale = False
        needsinfo_expired = False
        if 'needs_info' in self.issue.current_labels:
            time_delta = today - needsinfo_last_applied
            needsinfo_age = time_delta.days
            if needsinfo_age > self.RENOTIFY_INTERVAL:
                needsinfo_stale = True
            if needsinfo_age > self.RENOTIFY_EXPIRE:
                needsinfo_expired = True

        # Should we be in waiting_on_maintainer mode?
        maintainer_waiting_on = False
        if (needsinfo_remove or not needsinfo_add) \
            or not was_needs_info \
            and not missing_sections:
            maintainer_waiting_on = True

        # Should we [re]notify the submitter?
        submitter_waiting_on = False
        submitter_to_ping = False
        submitter_to_reping = False
        if not maintainer_waiting_on:
            submitter_waiting_on = True

        if missing_sections:
            submitter_waiting_on = True
            maintainer_waiting_on = False

        # use [!]needs_info to set final state
        if ni_commands:
            if ni_commands[-1] == '!needs_info':
                submitter_waiting_on = False
                maintainer_waiting_on = True
            elif ni_commands[-1] == 'needs_info':
                submitter_waiting_on = True
                maintainer_waiting_on = False

        # Time to [re]ping maintainer?
        maintainer_to_ping = False
        maintainer_to_reping = False
        if maintainer_waiting_on:

            # if feature idea, extend the notification interval
            interval = self.RENOTIFY_INTERVAL
            if self.meta.get('issue_type', None) == 'feature idea' \
                or 'feature_idea' in self.issue.current_labels:

                interval = self.FEATURE_RENOTIFY_INTERVAL

            if maintainer_viewed and not maintainer_last_notified:
                time_delta = today - maintainer_last_viewed
                view_age = time_delta.days
                if view_age > interval:
                    maintainer_to_reping = True
            elif maintainer_last_notified:
                time_delta = today - maintainer_last_notified
                ping_age = time_delta.days
                if ping_age > interval:
                    maintainer_to_reping = True
            else:
                maintainer_to_ping = True

        # Time to [re]ping the submitter?
        if submitter_waiting_on:
            if submitter_last_notified:
                time_delta = today - submitter_last_notified
                notification_age = time_delta.days
                if notification_age > self.RENOTIFY_INTERVAL:
                    submitter_to_reping = True
                else:
                    submitter_to_reping = False
                submitter_to_ping = False
            else:
                submitter_to_ping = True
                submitter_to_reping = False

        # needs_contributor ...
        hfacts['needs_contributor'] = False
        for command in maintainer_commands:
            if command == 'needs_contributor':
                hfacts['needs_contributor'] = True
            elif command == '!needs_contributor':
                hfacts['needs_contributor'] = False

        hfacts['bot_broken'] = bot_broken
        hfacts['bot_skip'] = bot_skip
        hfacts['missing_sections'] = missing_sections
        hfacts['was_needsinfo'] = was_needs_info
        hfacts['needsinfo_age'] = needsinfo_age
        hfacts['needsinfo_stale'] = needsinfo_stale
        hfacts['needsinfo_expired'] = needsinfo_expired
        hfacts['needsinfo_add'] = needsinfo_add
        hfacts['needsinfo_remove'] = needsinfo_remove
        hfacts['notification_maintainers'] = self.get_module_maintainers() or 'ansible'
        hfacts['maintainer_last_notified'] = maintainer_last_notified
        hfacts['maintainer_commented'] = maintainer_commented
        hfacts['maintainer_viewed'] = maintainer_viewed
        hfacts['maintainer_subscribed'] = maintainer_subscribed
        hfacts['maintainer_command_needsinfo'] = 'needs_info' in maintainer_commands
        hfacts['maintainer_command_not_needsinfo'] = '!needs_info' in maintainer_commands
        hfacts['maintainer_waiting_on'] = maintainer_waiting_on
        hfacts['maintainer_to_ping'] = maintainer_to_ping
        hfacts['maintainer_to_reping'] = maintainer_to_reping
        hfacts['submitter'] = submitter
        hfacts['submitter_waiting_on'] = submitter_waiting_on
        hfacts['submitter_to_ping'] = submitter_to_ping
        hfacts['submitter_to_reping'] = submitter_to_reping

        hfacts['last_commentor_ismaintainer'] = last_commentor_ismaintainer
        hfacts['last_commentor_issubmitter'] = last_commentor_issubmitter
        hfacts['last_commentor'] = last_commentor

        return hfacts
Exemple #3
0
class TriageIssues(DefaultTriager):

    VALID_COMMANDS = ['needs_info', '!needs_info', 'notabug',
                      'bot_broken', 'bot_skip',
                      'wontfix', 'bug_resolved', 'resolved_by_pr',
                      'needs_contributor', 'duplicate_of']

    CLOSURE_COMMANDS = [
        'notabug',
        'wontfix',
        'bug_resolved',
        'resolved_by_pr',
        'duplicate_of'
    ]

    # re-notify interval for maintainers
    RENOTIFY_INTERVAL = 14

    # max limit for needs_info notifications
    RENOTIFY_EXPIRE = 56

    # re-notify interval by this number for features
    FEATURE_RENOTIFY_INTERVAL = 60

    # the max comments per week before ansibot becomes a "spammer"
    MAX_BOT_COMMENTS_PER_WEEK = 5

    def run(self, useapiwrapper=True):
        """Starts a triage run"""

        # how many issues have been processed
        self.icount = 0

        # Create the api connection
        if not useapiwrapper:
            # use the default non-caching connection
            self.repo = self._connect().get_repo(self._get_repo_path())
        else:
            # make use of the special caching wrapper for the api
            self.gh = self._connect()
            self.ghw = GithubWrapper(self.gh)
            self.repo = self.ghw.get_repo(self._get_repo_path())

        # extend the ignored labels by repo
        if hasattr(self, 'IGNORE_LABELS_ADD'):
            self.IGNORE_LABELS.extend(self.IGNORE_LABELS_ADD)

        if self.number:
            issue = self.repo.get_issue(int(self.number))
            self.issue = IssueWrapper(repo=self.repo, issue=issue, cachedir=self.cachedir)
            self.issue.get_events()
            self.issue.get_comments()
            self.process()
        else:

            last_run = None
            now = self.get_current_time()
            last_run_file = '~/.ansibullbot/cache'
            if self.github_repo == 'ansible':
                last_run_file += '/ansible/ansible/'
            else:
                last_run_file += '/ansible/ansible-modules-%s/' % self.github_repo
            last_run_file += 'issues/last_run.pickle'
            last_run_file = os.path.expanduser(last_run_file)
            if os.path.isfile(last_run_file):
                try:
                    with open(last_run_file, 'rb') as f:
                        last_run = pickle.load(f)
                except Exception as e:
                    print(e)

            if last_run and not self.no_since:
                self.debug('Getting issues updated/created since %s' % last_run)
                issues = self.repo.get_issues(since=last_run)
            else:
                self.debug('Getting ALL issues')
                issues = self.repo.get_issues()

            for issue in issues:
                self.icount += 1
                if self.start_at and issue.number > self.start_at:
                    continue
                if self.is_pr(issue):
                    continue
                self.issue = IssueWrapper(repo=self.repo, issue=issue, cachedir=self.cachedir)
                self.issue.get_events()
                self.issue.get_comments()
                action_res = self.process()
                if action_res:
                    while action_res['REDO']:
                        issue = self.repo.get_issue(int(issue.number))
                        self.issue = IssueWrapper(repo=self.repo, issue=issue, cachedir=self.cachedir)
                        self.issue.get_events()
                        self.issue.get_comments()
                        action_res = self.process()
                        if not action_res:
                            action_res = {'REDO': False}

            # save this run time
            with open(last_run_file, 'wb') as f:
                pickle.dump(now, f)

    @RateLimited
    def process(self, usecache=True):
        """Processes the Issue"""

        # basic processing
        self._process()

        # who maintains this?
        maintainers = []

        if self.meta.get('component_valid', False):
            correct_repo = self.match.get('repository')
            if correct_repo != self.github_repo:
                self.meta['correct_repo'] = False
            else:
                self.meta['correct_repo'] = True
                maintainers = self.get_module_maintainers()
                if not maintainers:
                    #issue_module_no_maintainer
                    #import epdb; epdb.st()
                    pass


        ###########################################################
        #                   Enumerate Actions
        ###########################################################

        self.keep_current_main_labels()
        self.debug(msg='desired_comments: %s' % self.issue.desired_comments)

        self.process_history(usecache=usecache)
        self.debug(msg='desired_comments: %s' % self.issue.desired_comments)

        self.create_actions()
        self.debug(msg='desired_comments: %s' % self.issue.desired_comments)

        print("Module: %s" % self.module)
        if self.match:
            print("MModule: %s" % self.match['name'])
        else:
            print("MModule: %s" % self.match)
        print("Maintainer(s): %s" \
            % ', '.join(self.get_module_maintainers(expand=False)))
        print("Submitter: %s" % self.issue.get_submitter())
        print("Total Comments: %s" % len(self.issue.current_comments))
        self.print_comment_list()
        print("Current Labels: %s" % ', '.join(sorted(self.issue.current_labels)))

        # invoke the wizard
        import pprint; pprint.pprint(self.actions)
        action_meta = self.apply_actions()
        return action_meta


    def create_actions(self):
        """Create actions from the desired label/unlabel/comment actions"""

        # do nothing for bot_skip
        if self.meta['bot_skip']:
            return

        if 'bot_broken' in self.issue.desired_labels:
            # If the bot is broken, do nothing other than set the broken label
            self.actions['comments'] = []
            self.actions['newlabel'] = []
            self.actions['unlabel'] = []
            self.actions['close'] = False
            if not 'bot_broken' in self.issue.current_labels:
                self.actions['newlabel'] = ['bot_broken']
            return

        # add the version labels
        self.create_label_version_actions()

        if self.issue.desired_state != self.issue.instance.state:
            if self.issue.desired_state == 'closed':
                # close the issue ...
                self.actions['close'] = True

                # We only want up to 1 comment when an issue is closed
                if 'issue_closure' in self.issue.desired_comments:
                    self.issue.desired_comments = ['issue_closure']
                elif 'issue_deprecated_module' in self.issue.desired_comments:
                    self.issue.desired_comments = ['issue_deprecated_module']
                else:
                    self.issue.desired_comments = []

                if self.issue.desired_comments:
                    comment = self.render_comment(
                                boilerplate=self.issue.desired_comments[0]
                              )
                    self.actions['comments'].append(comment)
                return

        resolved_desired_labels = []
        for desired_label in self.issue.desired_labels:
            resolved_desired_label = self.issue.resolve_desired_labels(
                desired_label
            )
            if desired_label != resolved_desired_label:
                resolved_desired_labels.append(resolved_desired_label)
                if (resolved_desired_label not in self.issue.get_current_labels()):
                    self.issue.add_desired_comment(desired_label)
                    self.actions['newlabel'].append(resolved_desired_label)
            else:
                resolved_desired_labels.append(desired_label)
                if desired_label not in self.issue.get_current_labels():
                    self.actions['newlabel'].append(desired_label)
                    '''
                    if os.path.exists("templates/" + 'issue_' + desired_label + ".j2"):
                        self.issue.add_desired_comment('issue_' + desired_label)
                    '''

        for current_label in self.issue.get_current_labels():
            if current_label in self.IGNORE_LABELS:
                continue
            if current_label not in resolved_desired_labels:
                self.actions['unlabel'].append(current_label)

        # should only make one comment at a time
        if len(self.issue.desired_comments) > 1:
            if 'issue_wrong_repo' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_wrong_repo']
            elif 'issue_invalid_module' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_invalid_module']
            elif 'issue_module_no_maintainer' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_module_no_maintainer']
            elif 'issue_needs_info' in self.issue.desired_comments \
                and 'needs_info' in self.issue.desired_labels:
                self.issue.desired_comments = ['issue_needs_info']
            elif 'issue_needs_info' in self.issue.desired_comments \
                and 'needs_info' not in self.issue.desired_labels:
                self.issue.desired_comments.remove('issue_needs_info')
            else:
                import epdb; epdb.st()

        # Do not comment needs_info if it's not a label
        if 'issue_needs_info' in self.issue.desired_comments \
            and not 'needs_info' in self.issue.desired_labels:
            self.issue.desired_comments.remove('issue_needs_info')

        # render the comments
        for boilerplate in self.issue.desired_comments:
            comment = self.render_comment(boilerplate=boilerplate)
            self.debug(msg=boilerplate)
            self.actions['comments'].append(comment)

        # do not re-comment
        for idx,comment in enumerate(self.actions['comments']):
            if self.issue.current_comments:
                if self.issue.current_comments[-1].body == comment:
                    self.debug(msg="Removing repeat comment from actions")
                    self.actions['comments'].remove(comment)
        #import epdb; epdb.st()


    def create_label_version_actions(self):
        if not self.ansible_label_version:
            return
        expected = 'affects_%s' % self.ansible_label_version
        if expected not in self.valid_labels:
            print("NEED NEW LABEL: %s" % expected)
            import epdb; epdb.st()

        candidates = [x for x in self.issue.current_labels]
        candidates = [x for x in candidates if x.startswith('affects_')]
        candidates = [x for x in candidates if x != expected]
        if len(candidates) > 0:
            #for cand in candidates:
            #    self.issue.pop_desired_label(name=cand)
            #return
            pass
        else:
            if expected not in self.issue.current_labels \
                or expected not in self.issue.desired_labels:
                self.issue.add_desired_label(name=expected)
        #import epdb; epdb.st()


    def create_commment_actions(self):
        '''Render desired comment templates'''

        # should only make one comment at a time
        if len(self.issue.desired_comments) > 1:
            if 'issue_wrong_repo' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_wrong_repo']
            elif 'issue_invalid_module' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_invalid_module']
            elif 'issue_module_no_maintainer' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_module_no_maintainer']
            elif 'issue_needs_info' in self.issue.desired_comments \
                and 'needs_info' in self.issue.desired_labels:
                self.issue.desired_comments = ['issue_needs_info']
            elif 'issue_needs_info' in self.issue.desired_comments \
                and 'needs_info' not in self.issue.desired_labels:
                self.issue.desired_comments.remove('issue_needs_info')
            else:
                import epdb; epdb.st()

        # Do not comment needs_info if it's not a label
        if 'issue_needs_info' in self.issue.desired_comments \
            and not 'needs_info' in self.issue.desired_labels:
            self.issue.desired_comments.remove('issue_needs_info')

        # render the comments
        for boilerplate in self.issue.desired_comments:
            comment = self.render_comment(boilerplate=boilerplate)
            self.debug(msg=boilerplate)
            self.actions['comments'].append(comment)

        # do not re-comment
        for idx,comment in enumerate(self.actions['comments']):
            if self.issue.current_comments:
                if self.issue.current_comments[-1].body == comment:
                    self.debug(msg="Removing repeat comment from actions")
                    self.actions['comments'].remove(comment)


    def create_label_actions(self):
        """Create actions from the desired label/unlabel/comment actions"""

        if 'bot_broken' in self.issue.desired_labels:
            # If the bot is broken, do nothing other than set the broken label
            self.actions['comments'] = []
            self.actions['newlabel'] = []
            self.actions['unlabel'] = []
            self.actions['close'] = False
            if not 'bot_broken' in self.issue.current_labels:
                self.actions['newlabel'] = ['bot_broken']
            return

        if self.meta['bot_skip']:
            return

        resolved_desired_labels = []
        for desired_label in self.issue.desired_labels:
            resolved_desired_label = self.issue.resolve_desired_labels(
                desired_label
            )
            if desired_label != resolved_desired_label:
                resolved_desired_labels.append(resolved_desired_label)
                if (resolved_desired_label not in self.issue.get_current_labels()):
                    self.issue.add_desired_comment(desired_label)
                    self.actions['newlabel'].append(resolved_desired_label)
            else:
                resolved_desired_labels.append(desired_label)
                if desired_label not in self.issue.get_current_labels():
                    self.actions['newlabel'].append(desired_label)

        for current_label in self.issue.get_current_labels():
            if current_label in self.IGNORE_LABELS:
                continue
            if current_label not in resolved_desired_labels:
                self.actions['unlabel'].append(current_label)



    def process_history(self, usecache=True):
        '''Steps through all known meta about the issue and decides what to do'''

        self.meta.update(self.get_facts())
        self.add_desired_labels_by_issue_type()
        self.add_desired_labels_by_ansible_version()
        self.add_desired_labels_by_namespace()

        #################################################
        # FINAL LOGIC LOOP
        #################################################

        if self.meta['bot_broken']:
            self.debug(msg='broken bot stanza')
            self.issue.add_desired_label('bot_broken')

        elif self.meta['bot_skip']:

            self.debug(msg='bot skip stanza')

            # clear out all actions and do nothing
            for k,v in self.actions.iteritems():
                if type(v) == list:
                    self.actions[k] = []
            self.actions['close'] = False

        elif self.meta['bot_spam']:

            self.debug(msg='bot spam stanza')

            # clear out all actions and do nothing
            for k,v in self.actions.iteritems():
                if type(v) == list:
                    self.actions[k] = []
            self.actions['close'] = False

            # do we mark this somehow?
            self.issue.add_desired_label('bot_broken')


        elif self.match and self.match.get('deprecated', False) \
            and 'feature_idea' in self.issue.desired_labels:

            self.debug(msg='deprecated module stanza')

            # Make the deprecated comment
            self.issue.desired_comments = ['issue_deprecated_module']

            # Close the issue ...
            self.issue.set_desired_state('closed')

        elif not self.meta['maintainers_known'] and self.meta['valid_module']:

            self.debug(msg='unknown maintainer stanza')
            self.issue.desired_comments = ['issue_module_no_maintainer']

        elif self.meta['maintainer_closure']:

            self.debug(msg='maintainer closure stanza')

            # Need to close the issue ...
            self.issue.set_desired_state('closed')

        elif self.meta['new_module_request']:

            self.debug(msg='new module request stanza')
            self.issue.desired_comments = []
            for label in self.issue.current_labels:
                if not label in self.issue.desired_labels:
                    self.issue.desired_labels.append(label)

        elif not self.meta['correct_repo']:

            self.debug(msg='wrong repo stanza')
            self.issue.desired_comments = ['issue_wrong_repo']
            self.actions['close'] = True

        elif not self.meta['valid_module'] and \
            not self.meta['maintainer_command_needsinfo']:

            self.debug(msg='invalid module stanza')

            self.issue.add_desired_label('needs_info')
            if 'issue_invalid_module' not in self.issue.current_bot_comments \
                and not 'issue_needs_info' in self.issue.current_bot_comments:
                self.issue.desired_comments = ['issue_invalid_module']

        elif not self.meta['notification_maintainers'] and \
            not self.meta['maintainer_command_needsinfo']:

            self.debug(msg='no maintainer stanza')

            self.issue.add_desired_label('waiting_on_maintainer')
            self.issue.add_desired_comment("issue_module_no_maintainer")

        elif self.meta['maintainer_command'] == 'needs_contributor':

            # maintainer can't or won't fix this, but would like someone else to
            self.debug(msg='maintainer needs contributor stanza')
            self.issue.add_desired_label('waiting_on_contributor')

        elif self.meta['maintainer_waiting_on']:

            self.debug(msg='maintainer wait stanza')

            self.issue.add_desired_label('waiting_on_maintainer')
            if len(self.issue.current_comments) == 0:
                # new issue
                if self.meta['issue_type']:
                    if self.meta['submitter'] not in self.meta['notification_maintainers']:
                        # ping the maintainer
                        self.issue.add_desired_comment('issue_new')
                    else:
                        # do not send intial ping to maintainer if also submitter
                        if 'issue_new' in self.issue.desired_comments:
                            self.issue.desired_comments.remove('issue_new')
            else:
                # old issue -- renotify
                if not self.match['deprecated'] and self.meta['notification_maintainers']:
                    if self.meta['maintainer_to_ping']:
                        self.issue.add_desired_comment("issue_notify_maintainer")
                    elif self.meta['maintainer_to_reping']:
                        self.issue.add_desired_comment("issue_renotify_maintainer")

        elif self.meta['submitter_waiting_on']:

            self.debug(msg='submitter wait stanza')

            if 'waiting_on_maintainer' in self.issue.desired_labels:
                self.issue.desired_labels.remove('waiting_on_maintainer')

            if (self.meta['needsinfo_add'] or self.meta['missing_sections']) \
                or (not self.meta['needsinfo_remove'] and self.meta['missing_sections']) \
                or (self.meta['needsinfo_add'] and not self.meta['missing_sections']):


                #import epdb; epdb.st()
                self.issue.add_desired_label('needs_info')
                if len(self.issue.current_comments) == 0 or \
                    not self.meta['maintainer_commented']:

                    if self.issue.current_bot_comments:
                        if 'issue_needs_info' not in self.issue.current_bot_comments:
                            self.issue.add_desired_comment("issue_needs_info")
                    else:
                        self.issue.add_desired_comment("issue_needs_info")

                # needs_info: warn if stale, close if expired
                elif self.meta['needsinfo_expired']:
                    self.issue.add_desired_comment("issue_closure")
                    self.issue.set_desired_state('closed')
                elif self.meta['needsinfo_stale'] \
                    and (self.meta['submitter_to_ping'] or self.meta['submitter_to_reping']):
                    self.issue.add_desired_comment("issue_pending_closure")


    def get_history_facts(self, usecache=True):
        return self.get_facts(usecache=usecache)

    def get_facts(self, usecache=True):
        '''Only used by the ansible/ansible triager at the moment'''
        hfacts = {}
        today = self.get_current_time()

        self.history = HistoryWrapper(
                        self.issue,
                        usecache=usecache,
                        cachedir=self.cachedir
                       )

        # what was the last commment?
        bot_broken = False
        if self.issue.current_comments:
            for comment in self.issue.current_comments:
                if 'bot_broken' in comment.body:
                    bot_broken = True

        # did someone from ansible want this issue skipped?
        bot_skip = False
        if self.issue.current_comments:
            for comment in self.issue.current_comments:
                if 'bot_skip' in comment.body and comment.user.login in self.ansible_members:
                    bot_skip = True

        #########################################################
        # Facts for new "triage" workflow
        #########################################################

        # these logins don't count for the next set of facts
        exclude_logins = ['ansibot', 'gregdek', self.issue.instance.user.login]

        # Has a human (not ansibot + not submitter) ever changed labels?
        lchanges = [x for x in self.history.history if x['event'] in ['labeled', 'unlabeled'] and x['actor'] not in exclude_logins]
        if lchanges:
            hfacts['human_labeled'] = True
        else:
            hfacts['human_labeled'] = False

        # Has a human (not ansibot + not submitter) ever changed the title?
        tchanges = [x for x in self.history.history if x['event'] in ['renamed'] and x['actor'] not in exclude_logins]
        if tchanges:
            hfacts['human_retitled'] = True
        else:
            hfacts['human_retitled'] = False

        # Has a human (not ansibot + not submitter) ever changed the description?
        # ... github doesn't record an event for this. Lame.

        # Has a human (not ansibot + not submitter) ever set a milestone?
        mchanges = [x for x in self.history.history if x['event'] in ['milestoned', 'demilestoned'] and x['actor'] not in exclude_logins]
        if mchanges:
            hfacts['human_milestoned'] = True
        else:
            hfacts['human_milestoned'] = False

        # Has a human (not ansibot + not submitter) ever set/unset assignees?
        achanges = [x for x in self.history.history if x['event'] in ['assigned', 'unassigned'] and x['actor'] not in exclude_logins]
        if achanges:
            hfacts['human_assigned'] = True
        else:
            hfacts['human_assigned'] = False

        #########################################################

        # Has the bot been overzealous with comments?
        hfacts['bot_spam'] = False
        bcg = self.history.get_user_comments_groupby('ansibot', groupby='w')
        for k,v in bcg.iteritems():
            if v >= self.MAX_BOT_COMMENTS_PER_WEEK:
                hfacts['bot_spam'] = True

        # Is this a new module?
        hfacts['new_module_request'] = False
        if 'feature_idea' in self.issue.desired_labels:
            if self.template_data['component name'] == 'new':
                hfacts['new_module_request'] = True

        # who made this and when did they last comment?
        submitter = self.issue.get_submitter()
        submitter_last_commented = self.history.last_commented_at(submitter)
        if not submitter_last_commented:
            submitter_last_commented = self.issue.instance.created_at
            #import epdb; epdb.st()
        submitter_last_comment = self.history.last_comment(submitter)
        submitter_last_notified = self.history.last_notified(submitter)

        # what did they not provide?
        missing_sections = self.issue.get_missing_sections()

        # Is this a valid module?
        if self.match:
            self.meta['valid_module'] = True
        else:
            self.meta['valid_module'] = False

        # Filed in the right place?
        if self.meta['valid_module']:
            if self.match['repository'] != self.github_repo:
                hfacts['correct_repo'] = False
            else:
                hfacts['correct_repo'] = True
        else:
            hfacts['correct_repo'] = True

        # DEBUG + FIXME - speeds up bulk triage
        if 'component name' in missing_sections \
            and (self.match or self.github_repo == 'ansible'):
            missing_sections.remove('component name')
        #import epdb; epdb.st()

        # Who are the maintainers?
        maintainers = [x for x in self.get_module_maintainers()]
        #hfacts['maintainers'] = maintainers
        #import epdb; epdb.st()

        # Set a fact to indicate that we know the maintainer
        self.meta['maintainers_known'] = False
        if maintainers:
            self.meta['maintainers_known'] = True

        if 'ansible' in maintainers:
            maintainers.remove('ansible')
        maintainers.extend(self.ansible_members)
        if 'ansibot' in maintainers:
            maintainers.remove('ansibot')
        if submitter in maintainers:
            maintainers.remove(submitter)
        maintainers = sorted(set(maintainers))

        # Has maintainer been notified? When?
        notification_maintainers = [x for x in self.get_module_maintainers()]
        if 'ansible' in notification_maintainers:
            notification_maintainers.extend(self.ansible_members)
        if 'ansibot' in notification_maintainers:
            notification_maintainers.remove('ansibot')
        hfacts['notification_maintainers'] = notification_maintainers
        maintainer_last_notified = self.history.\
                    last_notified(notification_maintainers)

        # Has maintainer viewed issue?
        maintainer_viewed = self.history.has_viewed(maintainers)
        maintainer_last_viewed = self.history.last_viewed_at(maintainers)
        #import epdb; epdb.st()

        # Has maintainer been mentioned?
        maintainer_mentioned = self.history.is_mentioned(maintainers)

        # Has maintainer viewed issue?
        maintainer_viewed = self.history.has_viewed(maintainers)

        # Has the maintainer ever responded?
        maintainer_commented = self.history.has_commented(maintainers)
        maintainer_last_commented = self.history.last_commented_at(maintainers)
        maintainer_last_comment = self.history.last_comment(maintainers)
        maintainer_comments = self.history.get_user_comments(maintainers)
        #import epdb; epdb.st()

        # Was the maintainer the last commentor?
        last_commentor_ismaintainer = False
        last_commentor_issubmitter = False
        last_commentor = self.history.last_commentor()
        if last_commentor in maintainers and last_commentor != self.github_user:
            last_commentor_ismaintainer = True
        elif last_commentor == submitter:
            last_commentor_issubmitter = True

        # Did the maintainer issue a command?
        maintainer_commands = self.history.get_commands(maintainers,
                                                        self.VALID_COMMANDS)

        # Keep all commands
        hfacts['maintainer_commands'] = maintainer_commands
        # Set a bit for the last command given
        if hfacts['maintainer_commands']:
            hfacts['maintainer_command'] = hfacts['maintainer_commands'][-1]
        else:
            hfacts['maintainer_command'] = None

        # Is the last command a closure command?
        if hfacts['maintainer_command'] in self.CLOSURE_COMMANDS:
            hfacts['maintainer_closure'] = True
        else:
            hfacts['maintainer_closure'] = False

        # handle resolved_by_pr ...
        if 'resolved_by_pr' in maintainer_commands:
            maintainer_comments = self.history.get_user_comments(maintainers)
            maintainer_comments = [x for x in reversed(maintainer_comments) \
                                   if 'resolved_by_pr' in x]
            for comment in maintainer_comments:
                pr_number = extract_pr_number_from_comment(comment)
                hfacts['resolved_by_pr'] = {
                    'number': pr_number,
                    'merged': self.is_pr_merged(pr_number),
                }
                if not hfacts['resolved_by_pr']['merged']:
                    hfacts['maintainer_closure'] = False
                break

        # needs_info toggles
        ni_commands = [x for x in maintainer_commands if 'needs_info' in x]

        # Has the maintainer ever subscribed?
        maintainer_subscribed = self.history.has_subscribed(maintainers)

        # Was it ever needs_info?
        was_needs_info = self.history.was_labeled(label='needs_info')
        needsinfo_last_applied = self.history.label_last_applied('needs_info')
        needsinfo_last_removed = self.history.label_last_removed('needs_info')

        # Still needs_info?
        needsinfo_add = False
        needsinfo_remove = False

        if 'needs_info' in self.issue.current_labels:
            if not needsinfo_last_applied or not submitter_last_commented:
                import epdb; epdb.st()
            if submitter_last_commented > needsinfo_last_applied:
                needsinfo_add = False
                needsinfo_remove = True

        #if 'needs_info' in maintainer_commands and maintainer_last_commented:
        if ni_commands and maintainer_last_commented:
            if ni_commands[-1] == 'needs_info':
                #import epdb; epdb.st()
                if submitter_last_commented and maintainer_last_commented:
                    if submitter_last_commented > maintainer_last_commented:
                        needsinfo_add = False
                        needsinfo_remove = True
                else:
                    needsinfo_add = True
                    needsinfo_remove = False
            else:
                needsinfo_add = False
                needsinfo_remove = True

        # Save existing needs_info if not time to remove ...
        if 'needs_info' in self.issue.current_labels \
            and not needsinfo_add \
            and not needsinfo_remove:
            needsinfo_add = True

        if ni_commands and maintainer_last_commented:
            if maintainer_last_commented > submitter_last_commented:
                if ni_commands[-1] == 'needs_info':
                    needsinfo_add = True
                    needsinfo_remove = False
                else:
                    needsinfo_add = False
                    needsinfo_remove = True
        #import epdb; epdb.st()

        # Is needs_info stale or expired?
        needsinfo_age = None
        needsinfo_stale = False
        needsinfo_expired = False
        if 'needs_info' in self.issue.current_labels:
            time_delta = today - needsinfo_last_applied
            needsinfo_age = time_delta.days
            if needsinfo_age > self.RENOTIFY_INTERVAL:
                needsinfo_stale = True
            if needsinfo_age > self.RENOTIFY_EXPIRE:
                needsinfo_expired = True

        # Should we be in waiting_on_maintainer mode?
        maintainer_waiting_on = False
        if (needsinfo_remove or not needsinfo_add) \
            or not was_needs_info \
            and not missing_sections:
            maintainer_waiting_on = True

        # Should we [re]notify the submitter?
        submitter_waiting_on = False
        submitter_to_ping = False
        submitter_to_reping = False
        if not maintainer_waiting_on:
            submitter_waiting_on = True

        if missing_sections:
            submitter_waiting_on = True
            maintainer_waiting_on = False

        # use [!]needs_info to set final state
        if ni_commands:
            if ni_commands[-1] == '!needs_info':
                submitter_waiting_on = False
                maintainer_waiting_on = True
            elif ni_commands[-1] == 'needs_info':
                submitter_waiting_on = True
                maintainer_waiting_on = False

        # Time to [re]ping maintainer?
        maintainer_to_ping = False
        maintainer_to_reping = False
        if maintainer_waiting_on:

            # if feature idea, extend the notification interval
            interval = self.RENOTIFY_INTERVAL
            if self.meta.get('issue_type', None) == 'feature idea' \
                or 'feature_idea' in self.issue.current_labels:

                interval = self.FEATURE_RENOTIFY_INTERVAL

            if maintainer_viewed and not maintainer_last_notified:
                time_delta = today - maintainer_last_viewed
                view_age = time_delta.days
                if view_age > interval:
                    maintainer_to_reping = True
            elif maintainer_last_notified:
                time_delta = today - maintainer_last_notified
                ping_age = time_delta.days
                if ping_age > interval:
                    maintainer_to_reping = True
            else:
                maintainer_to_ping = True

        # Time to [re]ping the submitter?
        if submitter_waiting_on:
            if submitter_last_notified:
                time_delta = today - submitter_last_notified
                notification_age = time_delta.days
                if notification_age > self.RENOTIFY_INTERVAL:
                    submitter_to_reping = True
                else:
                    submitter_to_reping = False
                submitter_to_ping = False
            else:
                submitter_to_ping = True
                submitter_to_reping = False

        # needs_contributor ...
        hfacts['needs_contributor'] = False
        for command in maintainer_commands:
            if command == 'needs_contributor':
                hfacts['needs_contributor'] = True
            elif command == '!needs_contributor':
                hfacts['needs_contributor'] = False

        hfacts['bot_broken'] = bot_broken
        hfacts['bot_skip'] = bot_skip
        hfacts['missing_sections'] = missing_sections
        hfacts['was_needsinfo'] = was_needs_info
        hfacts['needsinfo_age'] = needsinfo_age
        hfacts['needsinfo_stale'] = needsinfo_stale
        hfacts['needsinfo_expired'] = needsinfo_expired
        hfacts['needsinfo_add'] = needsinfo_add
        hfacts['needsinfo_remove'] = needsinfo_remove
        hfacts['notification_maintainers'] = self.get_module_maintainers() or 'ansible'
        hfacts['maintainer_last_notified'] = maintainer_last_notified
        hfacts['maintainer_commented'] = maintainer_commented
        hfacts['maintainer_viewed'] = maintainer_viewed
        hfacts['maintainer_subscribed'] = maintainer_subscribed
        hfacts['maintainer_command_needsinfo'] = 'needs_info' in maintainer_commands
        hfacts['maintainer_command_not_needsinfo'] = '!needs_info' in maintainer_commands
        hfacts['maintainer_waiting_on'] = maintainer_waiting_on
        hfacts['maintainer_to_ping'] = maintainer_to_ping
        hfacts['maintainer_to_reping'] = maintainer_to_reping
        hfacts['submitter'] = submitter
        hfacts['submitter_waiting_on'] = submitter_waiting_on
        hfacts['submitter_to_ping'] = submitter_to_ping
        hfacts['submitter_to_reping'] = submitter_to_reping

        hfacts['last_commentor_ismaintainer'] = last_commentor_ismaintainer
        hfacts['last_commentor_issubmitter'] = last_commentor_issubmitter
        hfacts['last_commentor'] = last_commentor

        return hfacts
class TriageIssues(DefaultTriager):

    VALID_COMMANDS = [
        'needs_info', '!needs_info', 'notabug', 'wontfix', 'bug_resolved',
        'resolved_by_pr', 'needs_contributor', 'duplicate_of'
    ]

    def run(self, useapiwrapper=True):
        """Starts a triage run"""

        # how many issues have been processed
        self.icount = 0

        # Create the api connection
        if not useapiwrapper:
            # use the default non-caching connection
            self.repo = self._connect().get_repo(self._get_repo_path())
        else:
            # make use of the special caching wrapper for the api
            self.gh = self._connect()
            self.ghw = GithubWrapper(self.gh)
            self.repo = self.ghw.get_repo(self._get_repo_path())

        # extend the ignored labels by repo
        if hasattr(self, 'IGNORE_LABELS_ADD'):
            self.IGNORE_LABELS.extend(self.IGNORE_LABELS_ADD)

        if self.number:
            issue = self.repo.get_issue(int(self.number))
            self.issue = IssueWrapper(repo=self.repo,
                                      issue=issue,
                                      cachedir=self.cachedir)
            self.issue.get_events()
            self.issue.get_comments()
            self.process()
        else:

            last_run = None
            now = self.get_current_time()
            last_run_file = '~/.ansibullbot/cache'
            if self.github_repo == 'ansible':
                last_run_file += '/ansible/ansible/'
            else:
                last_run_file += '/ansible/ansible-modules-%s/' % self.github_repo
            last_run_file += 'issues/last_run.pickle'
            last_run_file = os.path.expanduser(last_run_file)
            if os.path.isfile(last_run_file):
                try:
                    with open(last_run_file, 'rb') as f:
                        last_run = pickle.load(f)
                except Exception as e:
                    print(e)

            if last_run and not self.no_since:
                self.debug('Getting issues updated/created since %s' %
                           last_run)
                issues = self.repo.get_issues(since=last_run)
            else:
                self.debug('Getting ALL issues')
                issues = self.repo.get_issues()

            for issue in issues:
                self.icount += 1
                if self.start_at and issue.number > self.start_at:
                    continue
                if self.is_pr(issue):
                    continue
                self.issue = IssueWrapper(repo=self.repo,
                                          issue=issue,
                                          cachedir=self.cachedir)
                self.issue.get_events()
                self.issue.get_comments()
                action_res = self.process()
                if action_res:
                    while action_res['REDO']:
                        issue = self.repo.get_issue(int(issue.number))
                        self.issue = IssueWrapper(repo=self.repo,
                                                  issue=issue,
                                                  cachedir=self.cachedir)
                        self.issue.get_events()
                        self.issue.get_comments()
                        action_res = self.process()
                        if not action_res:
                            action_res = {'REDO': False}

            # save this run time
            with open(last_run_file, 'wb') as f:
                pickle.dump(now, f)
            #import epdb; epdb.st()

    @ratecheck()
    def process(self, usecache=True):
        """Processes the Issue"""

        # basic processing
        self._process()

        # filed under the correct repository?
        this_repo = False
        correct_repo = None

        # who maintains this?
        maintainers = []

        component_isvalid = self.meta.get('component_isvalid', False)
        if not component_isvalid:
            pass
        else:
            correct_repo = self.module_indexer.\
                            get_repository_for_module(component)
            if correct_repo == self.github_repo:
                this_repo = True
                maintainers = self.get_module_maintainers()

        # Has the maintainer -ever- commented?
        maintainer_commented = False
        if component_isvalid:
            maintainer_commented = self.has_maintainer_commented()

        waiting_on_maintainer = False
        if component_isvalid:
            waiting_on_maintainer = self.is_waiting_on_maintainer()

        # How long ago did the maintainer last comment?
        maintainer_last_comment_age = -1
        if component_isvalid:
            maintainer_last_comment_age = self.age_of_last_maintainer_comment()

        ###########################################################
        #                   Enumerate Actions
        ###########################################################

        self.keep_current_main_labels()
        self.debug(msg='desired_comments: %s' % self.issue.desired_comments)

        self.process_history(usecache=usecache)
        self.debug(msg='desired_comments: %s' % self.issue.desired_comments)

        self.create_actions()
        self.debug(msg='desired_comments: %s' % self.issue.desired_comments)

        print("Module: %s" % self.module)
        if self.match:
            print("MModule: %s" % self.match['name'])
        else:
            print("MModule: %s" % self.match)
        print("Maintainer(s): %s" \
            % ', '.join(self.get_module_maintainers(expand=False)))
        print("Submitter: %s" % self.issue.get_submitter())
        print("Total Comments: %s" % len(self.issue.current_comments))
        for x in self.issue.current_comments:
            print("\t%s %s %s" % (x.created_at.isoformat(),
                        x.user.login,
                        [y for y in self.VALID_COMMANDS if y in x.body \
                        and not '!' + y in x.body] or ''))
        print("Current Labels: %s" %
              ', '.join(sorted(self.issue.current_labels)))

        import pprint
        pprint.pprint(self.actions)

        # invoke the wizard
        action_meta = self.apply_actions()
        return action_meta

    def create_actions(self):
        """Create actions from the desired label/unlabel/comment actions"""

        if 'bot_broken' in self.issue.desired_labels:
            # If the bot is broken, do nothing other than set the broken label
            self.actions['comments'] = []
            self.actions['newlabel'] = []
            self.actions['unlabel'] = []
            self.actions['close'] = False
            if not 'bot_broken' in self.issue.current_labels:
                self.actions['newlabel'] = ['bot_broken']
            return

        # add the version labels
        self.create_label_version_actions()

        if self.issue.desired_state != self.issue.instance.state:
            if self.issue.desired_state == 'closed':
                # close the issue ...
                self.actions['close'] = True
                if 'issue_closure' in self.issue.desired_comments:
                    comment = self.render_comment(boilerplate='issue_closure')
                    self.actions['comments'].append(comment)
                return

        resolved_desired_labels = []
        for desired_label in self.issue.desired_labels:
            resolved_desired_label = self.issue.resolve_desired_labels(
                desired_label)
            if desired_label != resolved_desired_label:
                resolved_desired_labels.append(resolved_desired_label)
                if (resolved_desired_label
                        not in self.issue.get_current_labels()):
                    self.issue.add_desired_comment(desired_label)
                    self.actions['newlabel'].append(resolved_desired_label)
            else:
                resolved_desired_labels.append(desired_label)
                if desired_label not in self.issue.get_current_labels():
                    self.actions['newlabel'].append(desired_label)
                    '''
                    if os.path.exists("templates/" + 'issue_' + desired_label + ".j2"):
                        self.issue.add_desired_comment('issue_' + desired_label)
                    '''

        for current_label in self.issue.get_current_labels():
            if current_label in self.IGNORE_LABELS:
                continue
            if current_label not in resolved_desired_labels:
                self.actions['unlabel'].append(current_label)

        # should only make one comment at a time
        if len(self.issue.desired_comments) > 1:
            if 'issue_wrong_repo' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_wrong_repo']
            elif 'issue_invalid_module' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_invalid_module']
            elif 'issue_module_no_maintainer' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_module_no_maintainer']
            elif 'issue_needs_info' in self.issue.desired_comments \
                and 'needs_info' in self.issue.desired_labels:
                self.issue.desired_comments = ['issue_needs_info']
            elif 'issue_needs_info' in self.issue.desired_comments \
                and 'needs_info' not in self.issue.desired_labels:
                self.issue.desired_comments.remove('issue_needs_info')
            else:
                import epdb
                epdb.st()

        # Do not comment needs_info if it's not a label
        if 'issue_needs_info' in self.issue.desired_comments \
            and not 'needs_info' in self.issue.desired_labels:
            self.issue.desired_comments.remove('issue_needs_info')

        # render the comments
        for boilerplate in self.issue.desired_comments:
            comment = self.render_comment(boilerplate=boilerplate)
            self.debug(msg=boilerplate)
            self.actions['comments'].append(comment)

        # do not re-comment
        for idx, comment in enumerate(self.actions['comments']):
            if self.issue.current_comments:
                if self.issue.current_comments[-1].body == comment:
                    self.debug(msg="Removing repeat comment from actions")
                    self.actions['comments'].remove(comment)
        #import epdb; epdb.st()

    def create_label_version_actions(self):
        if not self.ansible_label_version:
            return
        expected = 'affects_%s' % self.ansible_label_version
        if expected not in self.valid_labels:
            print("NEED NEW LABEL: %s" % expected)
            import epdb
            epdb.st()

        candidates = [x for x in self.issue.current_labels]
        candidates = [x for x in candidates if x.startswith('affects_')]
        candidates = [x for x in candidates if x != expected]
        if len(candidates) > 0:
            #for cand in candidates:
            #    self.issue.pop_desired_label(name=cand)
            #return
            pass
        else:
            if expected not in self.issue.current_labels \
                or expected not in self.issue.desired_labels:
                self.issue.add_desired_label(name=expected)
        #import epdb; epdb.st()

    def create_commment_actions(self):
        '''Render desired comment templates'''

        # should only make one comment at a time
        if len(self.issue.desired_comments) > 1:
            if 'issue_wrong_repo' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_wrong_repo']
            elif 'issue_invalid_module' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_invalid_module']
            elif 'issue_module_no_maintainer' in self.issue.desired_comments:
                self.issue.desired_comments = ['issue_module_no_maintainer']
            elif 'issue_needs_info' in self.issue.desired_comments \
                and 'needs_info' in self.issue.desired_labels:
                self.issue.desired_comments = ['issue_needs_info']
            elif 'issue_needs_info' in self.issue.desired_comments \
                and 'needs_info' not in self.issue.desired_labels:
                self.issue.desired_comments.remove('issue_needs_info')
            else:
                import epdb
                epdb.st()

        # Do not comment needs_info if it's not a label
        if 'issue_needs_info' in self.issue.desired_comments \
            and not 'needs_info' in self.issue.desired_labels:
            self.issue.desired_comments.remove('issue_needs_info')

        # render the comments
        for boilerplate in self.issue.desired_comments:
            comment = self.render_comment(boilerplate=boilerplate)
            self.debug(msg=boilerplate)
            self.actions['comments'].append(comment)

        # do not re-comment
        for idx, comment in enumerate(self.actions['comments']):
            if self.issue.current_comments:
                if self.issue.current_comments[-1].body == comment:
                    self.debug(msg="Removing repeat comment from actions")
                    self.actions['comments'].remove(comment)

    def create_label_actions(self):
        """Create actions from the desired label/unlabel/comment actions"""

        if 'bot_broken' in self.issue.desired_labels:
            # If the bot is broken, do nothing other than set the broken label
            self.actions['comments'] = []
            self.actions['newlabel'] = []
            self.actions['unlabel'] = []
            self.actions['close'] = False
            if not 'bot_broken' in self.issue.current_labels:
                self.actions['newlabel'] = ['bot_broken']
            return

        resolved_desired_labels = []
        for desired_label in self.issue.desired_labels:
            resolved_desired_label = self.issue.resolve_desired_labels(
                desired_label)
            if desired_label != resolved_desired_label:
                resolved_desired_labels.append(resolved_desired_label)
                if (resolved_desired_label
                        not in self.issue.get_current_labels()):
                    self.issue.add_desired_comment(desired_label)
                    self.actions['newlabel'].append(resolved_desired_label)
            else:
                resolved_desired_labels.append(desired_label)
                if desired_label not in self.issue.get_current_labels():
                    self.actions['newlabel'].append(desired_label)

        for current_label in self.issue.get_current_labels():
            if current_label in self.IGNORE_LABELS:
                continue
            if current_label not in resolved_desired_labels:
                self.actions['unlabel'].append(current_label)

    def process_history(self, usecache=True):
        self.meta = {}
        today = self.get_current_time()

        # Build the history
        self.debug(msg="Building event history ...")
        self.history = HistoryWrapper(self.issue,
                                      usecache=usecache,
                                      cachedir=self.cachedir)

        # what was the last commment?
        bot_broken = False
        if self.issue.current_comments:
            for comment in self.issue.current_comments:
                if 'bot_broken' in comment.body:
                    bot_broken = True

        # who made this and when did they last comment?
        submitter = self.issue.get_submitter()
        submitter_last_commented = self.history.last_commented_at(submitter)
        submitter_last_comment = self.history.last_comment(submitter)
        submitter_last_notified = self.history.last_notified(submitter)

        # what did they not provide?
        missing_sections = self.issue.get_missing_sections()
        if 'ansible version' in missing_sections:
            missing_sections.remove('ansible version')

        # DEBUG + FIXME - speeds up bulk triage
        if 'component name' in missing_sections and self.match:
            missing_sections.remove('component name')
        #import epdb; epdb.st()

        # Is this a valid module?
        valid_module = False
        correct_repo = True
        if self.match:
            valid_module = True
            if self.match['repository'] != self.github_repo:
                correct_repo = False
            #import epdb; epdb.st()

        # Do these after evaluating the module
        self.add_desired_labels_by_issue_type()
        if 'ansible version' in missing_sections:
            #self.debug(msg='desired_comments: %s' % self.issue.desired_comments)
            self.add_desired_labels_by_ansible_version()
        #self.debug(msg='desired_comments: %s' % self.issue.desired_comments)
        self.add_desired_labels_by_namespace()
        #self.debug(msg='desired_comments: %s' % self.issue.desired_comments)

        # Who are the maintainers?
        maintainers = [x for x in self.get_module_maintainers()]
        #if 'ansible' in maintainers:
        #    maintainers.remove('ansible')
        #    maintainers += self.ansible_members

        #print("MAINTAINERS: %s" % maintainers)
        if 'ansible' in maintainers:
            maintainers.remove('ansible')
        maintainers.extend(self.ansible_members)
        if 'ansibot' in maintainers:
            maintainers.remove('ansibot')
        if submitter in maintainers:
            maintainers.remove(submitter)
        maintainers = sorted(set(maintainers))

        # Has maintainer been notified? When?
        notification_maintainers = self.get_module_maintainers()
        if 'ansible' in notification_maintainers:
            notification_maintainers.extend(self.ansible_members)
        if 'ansibot' in notification_maintainers:
            notification_maintainers.remove('ansibot')
        maintainer_last_notified = self.history.\
                    last_notified(notification_maintainers)
        #import epdb; epdb.st()

        # Has maintainer viewed issue?
        maintainer_viewed = self.history.has_viewed(maintainers)
        maintainer_last_viewed = self.history.last_viewed_at(maintainers)
        #import epdb; epdb.st()

        # Has maintainer been mentioned?
        maintainer_mentioned = self.history.is_mentioned(maintainers)

        # Has maintainer viewed issue?
        maintainer_viewed = self.history.has_viewed(maintainers)

        # Has the maintainer ever responded?
        maintainer_commented = self.history.has_commented(maintainers)
        maintainer_last_commented = self.history.last_commented_at(maintainers)
        maintainer_last_comment = self.history.last_comment(maintainers)
        maintainer_comments = self.history.get_user_comments(maintainers)
        #import epdb; epdb.st()

        # Was the maintainer the last commentor?
        last_commentor_ismaintainer = False
        last_commentor_issubmitter = False
        last_commentor = self.history.last_commentor()
        if last_commentor in maintainers and last_commentor != self.github_user:
            last_commentor_ismaintainer = True
        elif last_commentor == submitter:
            last_commentor_issubmitter = True

        # Did the maintainer issue a command?
        maintainer_commands = self.history.get_commands(
            maintainers, self.VALID_COMMANDS)
        #import epdb; epdb.st()
        maintainer_command_close = False
        maintainer_command_needsinfo = False
        maintainer_command_not_needsinfo = False
        maintainer_command_notabug = False
        maintainer_command_wontfix = False
        maintainer_command_resolved_bug = False
        maintainer_command_resolved_pr = False
        maintainer_command_needscontributor = False
        maintainer_command_duplicateof = False
        if maintainer_commented and not maintainer_last_comment:
            print('ERROR: should have a comment from maintainer')
            import epdb
            epdb.st()
        elif maintainer_last_comment and last_commentor_ismaintainer:
            maintainer_last_comment = maintainer_last_comment.strip()
            if 'needs_info' in maintainer_last_comment \
                and not '!needs_info' in maintainer_last_comment:
                maintainer_command_needsinfo = True
            elif '!needs_info' in maintainer_last_comment:
                maintainer_command_not_needsinfo = True
            elif 'notabug' in maintainer_last_comment:
                maintainer_command_notabug = True
                maintainer_command_close = True
            elif 'wontfix' in maintainer_last_comment:
                maintainer_command_wontfix = True
                maintainer_command_close = True
            elif 'bug_resolved' in maintainer_last_comment:
                maintainer_command_resolved_bug = True
                maintainer_command_close = True
            elif 'resolved_by_pr' in maintainer_last_comment:
                maintainer_command_resolved_pr = True
                maintainer_command_close = True
            elif 'needs_contributor' in maintainer_last_comment:
                maintainer_command_needscontributor = True
            elif 'duplicate_of' in maintainer_last_comment:
                maintainer_command_duplicateof = True
                maintainer_command_close = True
        elif maintainer_commands:
            # are there any persistant commands?
            if 'needs_contributor' in maintainer_commands:
                maintainer_command_needscontributor = True
            elif not missing_sections and not submitter_last_commented and maintainer_commands[
                    -1] == 'needs_info':
                maintainer_command_needsinfo = True
            elif not missing_sections and not submitter_last_commented and maintainer_commands[
                    -1] == '!needs_info':
                maintainer_command_not_needsinfo = True
            #import epdb; epdb.st()
        #import epdb; epdb.st()

        # Has the maintainer ever subscribed?
        maintainer_subscribed = self.history.has_subscribed(maintainers)

        # Was it ever needs_info?
        was_needs_info = self.history.was_labeled(label='needs_info')
        needsinfo_last_applied = self.history.label_last_applied('needs_info')
        needsinfo_last_removed = self.history.label_last_removed('needs_info')
        #import epdb; epdb.st()

        # Still needs_info?
        needsinfo_add = False
        needsinfo_remove = False
        if 'needs_info' in self.issue.current_labels:
            if submitter_last_commented and needsinfo_last_applied:
                if submitter_last_commented > needsinfo_last_applied \
                    and not missing_sections:
                    needsinfo_remove = True
        if maintainer_command_needsinfo and maintainer_last_commented:
            if submitter_last_commented and maintainer_last_commented:
                if submitter_last_commented > maintainer_last_commented:
                    needsinfo_add = False
                    needsinfo_remove = True
            else:
                needsinfo_add = True
                needsinfo_remove = False

        # Is needs_info stale or expired?
        needsinfo_age = None
        needsinfo_stale = False
        needsinfo_expired = False
        if 'needs_info' in self.issue.current_labels:
            time_delta = today - needsinfo_last_applied
            needsinfo_age = time_delta.days
            if needsinfo_age > 14:
                needsinfo_stale = True
            if needsinfo_age > 56:
                needsinfo_expired = True

        # Should we be in waiting_on_maintainer mode?
        maintainer_waiting_on = False
        if (needsinfo_remove or not needsinfo_add) \
            or not was_needs_info \
            and not missing_sections:
            maintainer_waiting_on = True

        # Should we [re]notify the submitter?
        submitter_waiting_on = False
        submitter_to_ping = False
        submitter_to_reping = False
        if not maintainer_waiting_on:
            submitter_waiting_on = True

        if missing_sections:
            submitter_waiting_on = True
            maintainer_waiting_on = False
        else:
            if 'needs_info' in self.issue.current_labels \
                and not maintainer_command_not_needsinfo:
                needsinfo_remove = True
                submitter_waiting_on = False
                maintainer_waiting_on = True

        if maintainer_command_not_needsinfo:
            submitter_waiting_on = False
            maintainer_waiting_on = True

        if maintainer_command_needsinfo:
            submitter_waiting_on = True
            maintainer_waiting_on = False
            needsinfo_add = True
            needsinfo_remove = False

        # Time to [re]ping maintainer?
        maintainer_to_ping = False
        maintainer_to_reping = False
        if maintainer_waiting_on:
            #import epdb; epdb.st()
            if maintainer_viewed and not maintainer_last_notified:
                time_delta = today - maintainer_last_viewed
                view_age = time_delta.days
                if view_age > 14:
                    maintainer_to_reping = True
            elif maintainer_last_notified:
                time_delta = today - maintainer_last_notified
                ping_age = time_delta.days
                if ping_age > 14:
                    maintainer_to_reping = True
            else:
                maintainer_to_ping = True

        # Time to [re]ping the submitter?
        if submitter_waiting_on:
            if submitter_last_notified:
                time_delta = today - submitter_last_notified
                notification_age = time_delta.days
                if notification_age > 14:
                    submitter_to_reping = True
                else:
                    submitter_to_reping = False
                submitter_to_ping = False
            else:
                submitter_to_ping = True
                submitter_to_reping = False

        issue_type = self.template_data.get('issue type', None)
        issue_type = self.issue_type_to_label(issue_type)
        self.meta['issue_type'] = issue_type

        # new module requests need to disable everything
        #   https://github.com/ansible/ansible-modules-core/issues/4112
        #   https://github.com/ansible/ansible-modules-core/issues/2267
        #   https://github.com/ansible/ansible-modules-core/issues/2626
        #   https://github.com/ansible/ansible-modules-core/issues/645
        new_module_request = False
        if 'feature_idea' in self.issue.desired_labels:
            if self.template_data['component name'] == 'new':
                new_module_request = True

        # reset the maintainers
        maintainers = self.get_module_maintainers()

        #################################################
        # FINAL LOGIC LOOP
        #################################################

        if bot_broken:
            self.debug(msg='broken bot stanza')
            self.issue.add_desired_label('bot_broken')

        elif maintainer_command_close:

            self.debug(msg='maintainer closure stanza')

            # Need to close the issue ...
            self.issue.set_desired_state('closed')

        elif new_module_request:

            self.debug(msg='new module request stanza')
            self.issue.desired_comments = []
            for label in self.issue.current_labels:
                if not label in self.issue.desired_labels:
                    self.issue.desired_labels.append(label)

        elif not correct_repo:

            self.debug(msg='wrong repo stanza')
            self.issue.desired_comments = ['issue_wrong_repo']
            self.actions['close'] = True
            #import epdb; epdb.st()

        elif not valid_module and not maintainer_command_needsinfo:
            self.debug(msg='invalid module stanza')

            #import epdb; epdb.st()
            self.issue.add_desired_label('needs_info')
            if 'issue_invalid_module' not in self.issue.current_bot_comments \
                and not 'issue_needs_info' in self.issue.current_bot_comments:
                self.issue.desired_comments = ['issue_invalid_module']

        elif not maintainers and not maintainer_command_needsinfo:

            self.debug(msg='no maintainer stanza')

            self.issue.add_desired_label('waiting_on_maintainer')
            self.issue.add_desired_comment("issue_module_no_maintainer")

        elif maintainer_command_needscontributor:

            # maintainer can't or won't fix this, but would like someone else to
            self.debug(msg='maintainer needs contributor stanza')
            self.issue.add_desired_label('waiting_on_contributor')
            #import epdb; epdb.st()

        elif maintainer_waiting_on:

            self.debug(msg='maintainer wait stanza')

            self.issue.add_desired_label('waiting_on_maintainer')
            if len(self.issue.current_comments) == 0:
                if issue_type:
                    if submitter not in self.module_maintainers:
                        self.issue.add_desired_comment('issue_new')
                    else:
                        if 'issue_new' in self.issue.desired_comments:
                            self.issue.desired_comments.remove('issue_new')
                        #import epdb; epdb.st()
            else:
                if maintainers != ['DEPRECATED']:
                    if maintainer_to_ping and maintainers:
                        self.issue.add_desired_comment(
                            "issue_notify_maintainer")
                    elif maintainer_to_reping and maintainers:
                        self.issue.add_desired_comment(
                            "issue_renotify_maintainer")
                #import epdb; epdb.st()

        elif submitter_waiting_on:

            self.debug(msg='submitter wait stanza')
            #import epdb; epdb.st()

            if 'waiting_on_maintainer' in self.issue.desired_labels:
                self.issue.desired_labels.remove('waiting_on_maintainer')

            if (needsinfo_add or missing_sections) \
                or (not needsinfo_remove and missing_sections) \
                or (needsinfo_add and not missing_sections):

                self.issue.add_desired_label('needs_info')
                #import epdb; epdb.st()
                if len(self.issue.current_comments
                       ) == 0 or not maintainer_commented:
                    self.issue.add_desired_comment("issue_needs_info")

                # needs_info: warn if stale, close if expired
                elif needsinfo_expired:
                    self.issue.add_desired_comment("issue_closure")
                    self.issue.set_desired_state('closed')
                elif needsinfo_stale \
                    and (submitter_to_ping or submitter_to_reping):
                    self.issue.add_desired_comment("issue_pending_closure")
                #import epdb; epdb.st()
        #import epdb; epdb.st()

    def get_history_facts(self, usecache=True):

        hfacts = {}
        today = self.get_current_time()

        self.history = HistoryWrapper(self.issue,
                                      usecache=usecache,
                                      cachedir=self.cachedir)

        # what was the last commment?
        bot_broken = False
        if self.issue.current_comments:
            for comment in self.issue.current_comments:
                if 'bot_broken' in comment.body:
                    bot_broken = True

        # who made this and when did they last comment?
        submitter = self.issue.get_submitter()
        submitter_last_commented = self.history.last_commented_at(submitter)
        if not submitter_last_commented:
            submitter_last_commented = self.issue.instance.created_at
            #import epdb; epdb.st()
        submitter_last_comment = self.history.last_comment(submitter)
        submitter_last_notified = self.history.last_notified(submitter)

        # what did they not provide?
        missing_sections = self.issue.get_missing_sections()

        #if 'ansible version' in missing_sections:
        #    missing_sections.remove('ansible version')

        # DEBUG + FIXME - speeds up bulk triage
        if 'component name' in missing_sections \
            and (self.match or self.github_repo == 'ansible'):
            missing_sections.remove('component name')
        #import epdb; epdb.st()

        # Who are the maintainers?
        maintainers = [x for x in self.get_module_maintainers()]

        if 'ansible' in maintainers:
            maintainers.remove('ansible')
        maintainers.extend(self.ansible_members)
        if 'ansibot' in maintainers:
            maintainers.remove('ansibot')
        if submitter in maintainers:
            maintainers.remove(submitter)
        maintainers = sorted(set(maintainers))

        # Has maintainer been notified? When?
        notification_maintainers = self.get_module_maintainers()
        if 'ansible' in notification_maintainers:
            notification_maintainers.extend(self.ansible_members)
        if 'ansibot' in notification_maintainers:
            notification_maintainers.remove('ansibot')
        maintainer_last_notified = self.history.\
                    last_notified(notification_maintainers)

        # Has maintainer viewed issue?
        maintainer_viewed = self.history.has_viewed(maintainers)
        maintainer_last_viewed = self.history.last_viewed_at(maintainers)
        #import epdb; epdb.st()

        # Has maintainer been mentioned?
        maintainer_mentioned = self.history.is_mentioned(maintainers)

        # Has maintainer viewed issue?
        maintainer_viewed = self.history.has_viewed(maintainers)

        # Has the maintainer ever responded?
        maintainer_commented = self.history.has_commented(maintainers)
        maintainer_last_commented = self.history.last_commented_at(maintainers)
        maintainer_last_comment = self.history.last_comment(maintainers)
        maintainer_comments = self.history.get_user_comments(maintainers)
        #import epdb; epdb.st()

        # Was the maintainer the last commentor?
        last_commentor_ismaintainer = False
        last_commentor_issubmitter = False
        last_commentor = self.history.last_commentor()
        if last_commentor in maintainers and last_commentor != self.github_user:
            last_commentor_ismaintainer = True
        elif last_commentor == submitter:
            last_commentor_issubmitter = True

        # Did the maintainer issue a command?
        maintainer_commands = self.history.get_commands(
            maintainers, self.VALID_COMMANDS)
        # needs_info toggles
        ni_commands = [x for x in maintainer_commands if 'needs_info' in x]

        # Has the maintainer ever subscribed?
        maintainer_subscribed = self.history.has_subscribed(maintainers)

        # Was it ever needs_info?
        was_needs_info = self.history.was_labeled(label='needs_info')
        needsinfo_last_applied = self.history.label_last_applied('needs_info')
        needsinfo_last_removed = self.history.label_last_removed('needs_info')

        # Still needs_info?
        needsinfo_add = False
        needsinfo_remove = False

        if 'needs_info' in self.issue.current_labels:
            if not needsinfo_last_applied or not submitter_last_commented:
                import epdb
                epdb.st()
            if submitter_last_commented > needsinfo_last_applied:
                needsinfo_add = False
                needsinfo_remove = True

        #if 'needs_info' in maintainer_commands and maintainer_last_commented:
        if ni_commands and maintainer_last_commented:
            if ni_commands[-1] == 'needs_info':
                #import epdb; epdb.st()
                if submitter_last_commented and maintainer_last_commented:
                    if submitter_last_commented > maintainer_last_commented:
                        needsinfo_add = False
                        needsinfo_remove = True
                else:
                    needsinfo_add = True
                    needsinfo_remove = False
            else:
                needsinfo_add = False
                needsinfo_remove = True

        # Save existing needs_info if not time to remove ...
        if 'needs_info' in self.issue.current_labels \
            and not needsinfo_add \
            and not needsinfo_remove:
            needsinfo_add = True

        if ni_commands and maintainer_last_commented:
            if maintainer_last_commented > submitter_last_commented:
                if ni_commands[-1] == 'needs_info':
                    needsinfo_add = True
                    needsinfo_remove = False
                else:
                    needsinfo_add = False
                    needsinfo_remove = True
        #import epdb; epdb.st()

        # Is needs_info stale or expired?
        needsinfo_age = None
        needsinfo_stale = False
        needsinfo_expired = False
        if 'needs_info' in self.issue.current_labels:
            time_delta = today - needsinfo_last_applied
            needsinfo_age = time_delta.days
            if needsinfo_age > 14:
                needsinfo_stale = True
            if needsinfo_age > 56:
                needsinfo_expired = True

        # Should we be in waiting_on_maintainer mode?
        maintainer_waiting_on = False
        if (needsinfo_remove or not needsinfo_add) \
            or not was_needs_info \
            and not missing_sections:
            maintainer_waiting_on = True

        # Should we [re]notify the submitter?
        submitter_waiting_on = False
        submitter_to_ping = False
        submitter_to_reping = False
        if not maintainer_waiting_on:
            submitter_waiting_on = True

        if missing_sections:
            submitter_waiting_on = True
            maintainer_waiting_on = False

        # use [!]needs_info to set final state
        if ni_commands:
            if ni_commands[-1] == '!needs_info':
                submitter_waiting_on = False
                maintainer_waiting_on = True
            elif ni_commands[-1] == 'needs_info':
                submitter_waiting_on = True
                maintainer_waiting_on = False

        # Time to [re]ping maintainer?
        maintainer_to_ping = False
        maintainer_to_reping = False
        if maintainer_waiting_on:
            #import epdb; epdb.st()
            if maintainer_viewed and not maintainer_last_notified:
                time_delta = today - maintainer_last_viewed
                view_age = time_delta.days
                if view_age > 14:
                    maintainer_to_reping = True
            elif maintainer_last_notified:
                time_delta = today - maintainer_last_notified
                ping_age = time_delta.days
                if ping_age > 14:
                    maintainer_to_reping = True
            else:
                maintainer_to_ping = True

        # Time to [re]ping the submitter?
        if submitter_waiting_on:
            if submitter_last_notified:
                time_delta = today - submitter_last_notified
                notification_age = time_delta.days
                if notification_age > 14:
                    submitter_to_reping = True
                else:
                    submitter_to_reping = False
                submitter_to_ping = False
            else:
                submitter_to_ping = True
                submitter_to_reping = False

        hfacts['bot_broken'] = bot_broken
        hfacts['missing_sections'] = missing_sections
        hfacts['was_needsinfo'] = was_needs_info
        hfacts['needsinfo_age'] = needsinfo_age
        hfacts['needsinfo_stale'] = needsinfo_stale
        hfacts['needsinfo_expired'] = needsinfo_expired
        hfacts['needsinfo_add'] = needsinfo_add
        hfacts['needsinfo_remove'] = needsinfo_remove
        hfacts['notification_maintainers'] = self.get_module_maintainers(
        ) or 'ansible'
        hfacts['maintainer_last_notified'] = maintainer_last_notified
        hfacts['maintainer_commented'] = maintainer_commented
        hfacts['maintainer_viewed'] = maintainer_viewed
        hfacts['maintainer_subscribed'] = maintainer_subscribed
        hfacts[
            'maintainer_command_needsinfo'] = 'needs_info' in maintainer_commands
        hfacts[
            'maintainer_command_not_needsinfo'] = '!needs_info' in maintainer_commands
        hfacts['maintainer_waiting_on'] = maintainer_waiting_on
        hfacts['maintainer_to_ping'] = maintainer_to_ping
        hfacts['maintainer_to_reping'] = maintainer_to_reping
        hfacts['submitter'] = submitter
        hfacts['submitter_waiting_on'] = submitter_waiting_on
        hfacts['submitter_to_ping'] = submitter_to_ping
        hfacts['submitter_to_reping'] = submitter_to_reping

        hfacts['last_commentor_ismaintainer'] = last_commentor_ismaintainer
        hfacts['last_commentor_issubmitter'] = last_commentor_issubmitter
        hfacts['last_commentor'] = last_commentor

        #import epdb; epdb.st()
        return hfacts