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;
10
11async function main() {
12 const octokit = new Octokit({ auth: process.env["GITHUB_TOKEN"] });
13
14 if (!process.env["SLACK_ISSUE_RESPONSE_WEBHOOK_URL"]) {
15 throw new Error("SLACK_ISSUE_RESPONSE_WEBHOOK_URL is not set");
16 }
17
18 const webhook = new IncomingWebhook(
19 process.env["SLACK_ISSUE_RESPONSE_WEBHOOK_URL"],
20 );
21
22 const owner = "zed-industries";
23 const repo = "zed";
24 const staff = await octokit.paginate(octokit.rest.orgs.listMembers, {
25 org: owner,
26 per_page: 100,
27 });
28 let staffHandles = staff.map((member) => member.login);
29 let commenterFilters = staffHandles.map((name) => `-commenter:${name}`);
30 let authorFilters = staffHandles.map((name) => `-author:${name}`);
31
32 const q = [
33 `repo:${owner}/${repo}`,
34 "is:issue",
35 "state:open",
36 "created:>=2025-02-01",
37 "sort:created-asc",
38 ...commenterFilters,
39 ...authorFilters,
40 ];
41
42 const response = await octokit.rest.search.issuesAndPullRequests({
43 q: q.join("+"),
44 per_page: 100,
45 });
46
47 let issues = response.data.items;
48 let issueLines = issues.map((issue, index) => {
49 const formattedDate = new Date(issue.created_at).toLocaleDateString(
50 "en-US",
51 {
52 year: "numeric",
53 month: "short",
54 day: "numeric",
55 },
56 );
57 const sanitizedTitle = issue.title
58 .replaceAll("&", "&")
59 .replaceAll("<", "<")
60 .replaceAll(">", ">");
61
62 return `${index + 1}. ${formattedDate}: <${issue.html_url}|${sanitizedTitle}>\n`;
63 });
64
65 const sections = [];
66 /** @type {string[]} */
67 let currentSection = [];
68 let currentSectionLength = 0;
69
70 for (const issueLine of issueLines) {
71 if (currentSectionLength + issueLine.length <= SECTION_BLOCK_TEXT_LIMIT) {
72 currentSection.push(issueLine);
73 currentSectionLength += issueLine.length;
74 } else {
75 sections.push(currentSection);
76 currentSection = [];
77 currentSectionLength = 0;
78 }
79 }
80
81 if (currentSection.length > 0) {
82 sections.push(currentSection);
83 }
84
85 const blocks = sections.map((section) => ({
86 type: "section",
87 text: {
88 type: "mrkdwn",
89 text: section.join("").trimEnd(),
90 },
91 }));
92
93 await webhook.send({ blocks });
94}
95
96main().catch((error) => {
97 console.error("An error occurred:", error);
98 process.exit(1);
99});