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 teams = ["staff"];
 28  const githubHandleSet = new Set();
 29
 30  for (const team of teams) {
 31    const teamMembers = await octokit.paginate(
 32      octokit.rest.teams.listMembersInOrg,
 33      {
 34        org: owner,
 35        team_slug: team,
 36        per_page: 100,
 37      },
 38    );
 39
 40    for (const teamMember of teamMembers) {
 41      githubHandleSet.add(teamMember.login);
 42    }
 43  }
 44
 45  const githubHandles = Array.from(githubHandleSet);
 46  githubHandles.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
 47  const commenterFilters = githubHandles.map((name) => `-commenter:${name}`);
 48  const authorFilters = githubHandles.map((name) => `-author:${name}`);
 49  const twoDaysAgo = new Date();
 50  twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
 51  const twoDaysAgoString = twoDaysAgo.toISOString().split("T")[0];
 52  const dateRangeFilter = `2025-02-01..${twoDaysAgoString}`;
 53
 54  const q = [
 55    `repo:${owner}/${repo}`,
 56    "is:issue",
 57    "-type:feature",
 58    "-type:meta",
 59    "state:open",
 60    `created:${dateRangeFilter}`,
 61    "sort:created-asc",
 62    ...commenterFilters,
 63    ...authorFilters,
 64  ];
 65
 66  const issues = await octokit.paginate(
 67    octokit.rest.search.issuesAndPullRequests,
 68    {
 69      q: q.join("+"),
 70      per_page: 100,
 71    },
 72  );
 73  const issueLines = issues.map((issue, index) => {
 74    const formattedDate = new Date(issue.created_at).toLocaleDateString(
 75      "en-US",
 76      {
 77        year: "numeric",
 78        month: "short",
 79        day: "numeric",
 80      },
 81    );
 82    const sanitizedTitle = issue.title
 83      .replaceAll("&", "&")
 84      .replaceAll("<", "&lt;")
 85      .replaceAll(">", "&gt;");
 86
 87    return `${index + 1}. ${formattedDate}: <${issue.html_url}|${sanitizedTitle}>\n`;
 88  });
 89
 90  const sections = [];
 91  /** @type {string[]} */
 92  let currentSection = [];
 93  let currentSectionLength = 0;
 94
 95  for (const issueLine of issueLines) {
 96    if (currentSectionLength + issueLine.length <= SECTION_BLOCK_TEXT_LIMIT) {
 97      currentSection.push(issueLine);
 98      currentSectionLength += issueLine.length;
 99    } else {
100      sections.push(currentSection);
101      currentSection = [];
102      currentSectionLength = 0;
103    }
104  }
105
106  if (currentSection.length > 0) {
107    sections.push(currentSection);
108  }
109
110  const blocks = sections.map((section) => ({
111    type: "section",
112    text: {
113      type: "mrkdwn",
114      text: section.join("").trimEnd(),
115    },
116  }));
117
118  const issuesUrl = `${GITHUB_ISSUES_URL}?q=${encodeURIComponent(q.join(" "))}`;
119
120  blocks.push({
121    type: "section",
122    text: {
123      type: "mrkdwn",
124      text: `<${issuesUrl}|View on GitHub>`,
125    },
126  });
127
128  await webhook.send({ blocks });
129}
130
131main().catch((error) => {
132  console.error("An error occurred:", error);
133  process.exit(1);
134});