main.js

  1import { Octokit } from "@octokit/rest";
  2import { IncomingWebhook } from "@slack/webhook";
  3
  4/**
  5 * The maximum length of the `text` in a section block.
  6 *
  7 * [Slack Docs](https://api.slack.com/reference/block-kit/blocks#section)
  8 */
  9const SECTION_BLOCK_TEXT_LIMIT = 3000;
 10const GITHUB_ISSUES_URL = "https://github.com/zed-industries/zed/issues";
 11
 12async function main() {
 13  const octokit = new Octokit({
 14    auth: process.env["ISSUE_RESPONSE_GITHUB_TOKEN"],
 15  });
 16
 17  if (!process.env["SLACK_ISSUE_RESPONSE_WEBHOOK_URL"]) {
 18    throw new Error("SLACK_ISSUE_RESPONSE_WEBHOOK_URL is not set");
 19  }
 20
 21  const webhook = new IncomingWebhook(
 22    process.env["SLACK_ISSUE_RESPONSE_WEBHOOK_URL"],
 23  );
 24
 25  const owner = "zed-industries";
 26  const repo = "zed";
 27  const staff = await octokit.paginate(octokit.rest.teams.listMembersInOrg, {
 28    org: owner,
 29    team_slug: "staff",
 30    per_page: 100,
 31  });
 32  let staffHandles = staff.map((member) => member.login);
 33  let commenterFilters = staffHandles.map((name) => `-commenter:${name}`);
 34  let authorFilters = staffHandles.map((name) => `-author:${name}`);
 35
 36  const q = [
 37    `repo:${owner}/${repo}`,
 38    "is:issue",
 39    "state:open",
 40    "created:>=2025-02-01",
 41    "sort:created-asc",
 42    ...commenterFilters,
 43    ...authorFilters,
 44  ];
 45
 46  const searchQuery = q.join("+");
 47
 48  const response = await octokit.rest.search.issuesAndPullRequests({
 49    q: searchQuery,
 50    per_page: 100,
 51  });
 52
 53  let issues = response.data.items;
 54  let issueLines = issues.map((issue, index) => {
 55    const formattedDate = new Date(issue.created_at).toLocaleDateString(
 56      "en-US",
 57      {
 58        year: "numeric",
 59        month: "short",
 60        day: "numeric",
 61      },
 62    );
 63    const sanitizedTitle = issue.title
 64      .replaceAll("&", "&")
 65      .replaceAll("<", "&lt;")
 66      .replaceAll(">", "&gt;");
 67
 68    return `${index + 1}. ${formattedDate}: <${issue.html_url}|${sanitizedTitle}>\n`;
 69  });
 70
 71  const sections = [];
 72  /** @type {string[]} */
 73  let currentSection = [];
 74  let currentSectionLength = 0;
 75
 76  for (const issueLine of issueLines) {
 77    if (currentSectionLength + issueLine.length <= SECTION_BLOCK_TEXT_LIMIT) {
 78      currentSection.push(issueLine);
 79      currentSectionLength += issueLine.length;
 80    } else {
 81      sections.push(currentSection);
 82      currentSection = [];
 83      currentSectionLength = 0;
 84    }
 85  }
 86
 87  if (currentSection.length > 0) {
 88    sections.push(currentSection);
 89  }
 90
 91  const blocks = sections.map((section) => ({
 92    type: "section",
 93    text: {
 94      type: "mrkdwn",
 95      text: section.join("").trimEnd(),
 96    },
 97  }));
 98
 99  const issuesUrl = `${GITHUB_ISSUES_URL}?q=${encodeURIComponent(searchQuery)}`;
100
101  blocks.push({
102    type: "section",
103    text: {
104      type: "mrkdwn",
105      text: `<${issuesUrl}|View on GitHub>`,
106    },
107  });
108
109  await webhook.send({ blocks });
110}
111
112main().catch((error) => {
113  console.error("An error occurred:", error);
114  process.exit(1);
115});