async handleStaleIssue()

in functions/src/cron.ts [167:316]


  async handleStaleIssue(
    org: string,
    name: string,
    issue: types.internal.Issue,
    issueConfig: types.IssueCleanupConfig
  ): Promise<types.Action[]> {
    const actions: types.Action[] = [];

    const number = issue.number;
    const labelNames = issue.labels.map(label => label.name);

    const stateNeedsInfo = labelNames.includes(issueConfig.label_needs_info);
    const stateStale = labelNames.includes(issueConfig.label_stale);

    // If an issue is not labeled with either the stale or needs-info labels
    // then we don't need to do any cron processing on it.
    if (!(stateNeedsInfo || stateStale)) {
      return actions;
    }

    // If the issue has one of the specified labels to ignore, then we
    // never mark it as stale or close it automatically.
    let hasIgnoredLabel = false;
    const ignoredLabels = issueConfig.ignore_labels || [];
    ignoredLabels.forEach(label => {
      hasIgnoredLabel = hasIgnoredLabel || labelNames.includes(label);
    });

    if (hasIgnoredLabel) {
      log.debug(
        `Issue ${name}#${number} is ignored due to labels: ${JSON.stringify(
          labelNames
        )}`
      );
      return actions;
    }

    // We fetch the comments for the issue so we can determine when the last actions were taken.
    // We manually sort the API response by timestamp (newest to oldest) because the API
    // does not guarantee an order.
    let comments = await this.gh_client.getCommentsForIssue(org, name, number);
    comments = comments.sort(util.compareTimestamps).reverse();

    if (!comments || comments.length === 0) {
      console.log(`Issue ${name}#${number} has no comments.`);
      return actions;
    }

    // When the issue was marked stale, the bot will have left a comment with certain metadata
    const markStaleComment = comments.find(comment => {
      return comment.body.includes(EVT_MARK_STALE);
    });

    if (stateStale && !markStaleComment) {
      log.warn(
        `Issue ${name}/${number} is stale but no relevant comment was found.`
      );
    }

    if (stateNeedsInfo || stateStale) {
      log.debug(
        `Processing ${name}#${number} as needs-info or stale, labels=${JSON.stringify(
          labelNames
        )}`
      );
    }

    // The github webhook handler will automatically remove the needs-info label
    // if the author comments, so we can assume inside the cronjob that this has
    // not happened and just look at the date of the last comment.
    //
    // A comment by anyone in the last 7 days makes the issue non-stale.
    const lastCommentTime = util.createdDate(comments[0]);
    const shouldMarkStale =
      stateNeedsInfo &&
      util.workingDaysAgo(lastCommentTime) >= issueConfig.needs_info_days;

    const shouldClose =
      stateStale &&
      markStaleComment != undefined &&
      util.workingDaysAgo(util.createdDate(markStaleComment)) >=
        issueConfig.stale_days;

    if (shouldClose) {
      // 1) Add a comment about closing
      const addClosingComment = new types.GitHubCommentAction(
        org,
        name,
        number,
        this.getCloseComment(issue.user.login),
        false,
        `Comment after closing issue for being stale (comment at ${util.createdDate(
          markStaleComment!
        )}).`
      );
      actions.push(addClosingComment);

      // 2) Close the issue
      const closeIssue = new types.GitHubCloseAction(
        org,
        name,
        number,
        `Closing issue for being stale.`
      );
      actions.push(closeIssue);

      // 3) Add and remove labels (according to config)
      if (issueConfig.auto_close_labels) {
        for (const l of issueConfig.auto_close_labels.add) {
          actions.push(new types.GitHubAddLabelAction(org, name, number, l));
        }
        for (const l of issueConfig.auto_close_labels.remove) {
          actions.push(new types.GitHubRemoveLabelAction(org, name, number, l));
        }
      } else {
        // Default is to add 'closed-by-bot'
        actions.push(
          new types.GitHubAddLabelAction(org, name, number, "closed-by-bot")
        );
      }
    } else if (shouldMarkStale) {
      // We add the 'stale' label and also add a comment. Note that
      // if the issue was labeled 'needs-info' this label is not removed
      // here.
      const addStaleLabel = new types.GitHubAddLabelAction(
        org,
        name,
        number,
        issueConfig.label_stale,
        `Last comment was ${util.workingDaysAgo(
          lastCommentTime
        )} working days ago (${lastCommentTime}).`
      );
      const addStaleComment = new types.GitHubCommentAction(
        org,
        name,
        number,
        this.getMarkStaleComment(
          issue.user.login,
          issueConfig.needs_info_days,
          issueConfig.stale_days
        ),
        false,
        `Comment that goes alongside the stale label.`
      );
      actions.push(addStaleLabel, addStaleComment);
    }

    return actions;
  }