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("<", "<")
66 .replaceAll(">", ">");
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=${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});