Skip to content

Commit 6001cf8

Browse files
subatoiCopilot
andauthored
Include inline comments and submitted reviews in the daily non-employee digest (#64504)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 09469c3 commit 6001cf8

1 file changed

Lines changed: 122 additions & 42 deletions

File tree

‎.github/workflows/notify-non-employee-comments.yml‎

Lines changed: 122 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
name: Daily non-employee comment digest
1+
name: Daily non-employee comment and review digest
22

3-
# Include issue/PR conversation comments, not inline review comments.
3+
# Include conversation comments, inline review comments, and submitted reviews without their bodies.
44
on:
55
workflow_dispatch:
66
schedule:
@@ -51,45 +51,121 @@ jobs:
5151
core.info(`Building digest for ${since.toISOString()} to ${until.toISOString()}.`);
5252
5353
const authors = new Map();
54+
function addActivity({ author, createdAt, url, conversation, kind }) {
55+
const timestamp = Date.parse(createdAt);
56+
if (!(timestamp >= since.getTime() && timestamp < until.getTime())) {
57+
return;
58+
}
59+
if (!author) {
60+
core.warning('Skipping an activity whose author is unavailable.');
61+
return;
62+
}
63+
const login = author.login.toLowerCase();
64+
if (author.type === 'Bot' || author.__typename === 'Bot' ||
65+
['docs-bot', 'copilot'].includes(login)) {
66+
return;
67+
}
68+
if (!authors.has(login)) {
69+
authors.set(login, { login: author.login, activities: [] });
70+
}
71+
authors.get(login).activities.push({ url, conversation, kind });
72+
}
73+
5474
let page = 0;
5575
try {
56-
for await (const { data: comments } of github.paginate.iterator(
76+
for (const listComments of [
5777
github.rest.issues.listCommentsForRepo,
58-
{
59-
owner: 'github',
60-
repo: 'docs',
61-
since: since.toISOString(),
62-
sort: 'created',
63-
direction: 'asc',
64-
per_page: 100,
65-
},
66-
)) {
67-
core.info(`Fetched comments page ${++page} (${comments.length} comments).`);
68-
for (const comment of comments) {
69-
const createdAt = Date.parse(comment.created_at);
70-
if (!(createdAt >= since.getTime() && createdAt < until.getTime())) {
71-
continue;
78+
github.rest.pulls.listReviewCommentsForRepo,
79+
]) {
80+
for await (const { data: comments } of github.paginate.iterator(
81+
listComments,
82+
{
83+
owner: 'github',
84+
repo: 'docs',
85+
since: since.toISOString(),
86+
sort: 'created',
87+
direction: 'asc',
88+
per_page: 100,
89+
},
90+
)) {
91+
core.info(`Fetched comments page ${++page} (${comments.length} comments).`);
92+
for (const comment of comments) {
93+
addActivity({
94+
author: comment.user,
95+
createdAt: comment.created_at,
96+
url: comment.html_url,
97+
conversation: comment.issue_url ??
98+
comment.pull_request_url.replace('/pulls/', '/issues/'),
99+
kind: 'comment',
100+
});
72101
}
73-
const author = comment.user;
74-
if (!author) {
75-
core.warning('Skipping a comment whose author is unavailable.');
76-
continue;
102+
}
103+
}
104+
} catch (error) {
105+
throw new Error(`Fetching digest comments failed (HTTP ${error.status || 'unknown'}).`);
106+
}
107+
108+
const reviewFields = `
109+
fragment digestReviews on PullRequest {
110+
reviews(first: 100, after: $reviewsCursor) {
111+
nodes {
112+
author { login __typename }
113+
submittedAt
114+
url
77115
}
78-
const login = author.login.toLowerCase();
79-
if (author.type === 'Bot' || ['docs-bot', 'copilot'].includes(login)) {
80-
continue;
116+
pageInfo { hasNextPage endCursor }
117+
}
118+
}
119+
`;
120+
let pullRequestsCursor = null;
121+
try {
122+
do {
123+
const { repository } = await github.graphql(`
124+
query DigestPullRequests($pullRequestsCursor: String, $reviewsCursor: String) {
125+
repository(owner: "github", name: "docs") {
126+
pullRequests(first: 50, after: $pullRequestsCursor,
127+
orderBy: { field: UPDATED_AT, direction: DESC }) {
128+
nodes { id number updatedAt ...digestReviews }
129+
pageInfo { hasNextPage endCursor }
130+
}
131+
}
81132
}
82-
if (!authors.has(login)) {
83-
authors.set(login, { login: author.login, comments: [] });
133+
${reviewFields}
134+
`, { pullRequestsCursor, reviewsCursor: null });
135+
const pullRequests = repository.pullRequests;
136+
const recentPullRequests = pullRequests.nodes.filter(
137+
(pullRequest) => Date.parse(pullRequest.updatedAt) >= since.getTime(),
138+
);
139+
for (const pullRequest of recentPullRequests) {
140+
let reviews = pullRequest.reviews;
141+
while (true) {
142+
for (const review of reviews.nodes) {
143+
addActivity({
144+
author: review.author,
145+
createdAt: review.submittedAt,
146+
url: review.url,
147+
conversation: `https://api.github.com/repos/github/docs/issues/${pullRequest.number}`,
148+
kind: 'review',
149+
});
150+
}
151+
if (!reviews.pageInfo.hasNextPage) {
152+
break;
153+
}
154+
const { node } = await github.graphql(`
155+
query DigestReviewPage($id: ID!, $reviewsCursor: String) {
156+
node(id: $id) { ...digestReviews }
157+
}
158+
${reviewFields}
159+
`, { id: pullRequest.id, reviewsCursor: reviews.pageInfo.endCursor });
160+
reviews = node.reviews;
84161
}
85-
authors.get(login).comments.push({
86-
url: comment.html_url,
87-
conversation: comment.issue_url,
88-
});
89162
}
90-
}
163+
// Updated ordering includes closed/merged PRs and lets us stop at the cutoff.
164+
pullRequestsCursor = recentPullRequests.length === pullRequests.nodes.length &&
165+
pullRequests.pageInfo.hasNextPage ? pullRequests.pageInfo.endCursor : null;
166+
} while (pullRequestsCursor);
91167
} catch (error) {
92-
throw new Error(`Fetching digest comments failed (HTTP ${error.status || 'unknown'}).`);
168+
throw new Error(`Fetching digest reviews failed (HTTP ${error.status || 'unknown'}).`);
93169
}
94170
95171
const nonEmployees = [];
@@ -110,23 +186,27 @@ jobs:
110186
}
111187
}
112188
113-
const totalComments = nonEmployees.reduce((sum, author) => sum + author.comments.length, 0);
114-
core.info(`Found ${totalComments} comments by ${nonEmployees.length} non-employees.`);
115-
if (totalComments === 0) {
189+
const activities = nonEmployees.flatMap((author) => author.activities);
190+
const totalComments = activities.filter((activity) => activity.kind === 'comment').length;
191+
const totalReviews = activities.length - totalComments;
192+
core.info(`Found ${totalComments} comments and ${totalReviews} reviews by ${nonEmployees.length} non-employees.`);
193+
if (activities.length === 0) {
116194
return;
117195
}
118196
119-
let message = `Daily non-employee comment digest for github/docs\n` +
197+
let message = `Daily non-employee comment and review digest for github/docs\n` +
120198
`${since.toISOString()} to ${until.toISOString()} (UTC)\n` +
121-
`Non-employee users: ${nonEmployees.length}; comments: ${totalComments}\n` +
122-
'Comment bodies omitted; follow the links to read them on GitHub.';
123-
const shortened = '\n\n[Digest shortened to fit one Slack message; some authors or comment links are omitted.]';
199+
`Non-employee users: ${nonEmployees.length}; comments: ${totalComments}; reviews: ${totalReviews}\n` +
200+
'Comment and review bodies omitted; follow the links to read them on GitHub.';
201+
const shortened = '\n\n[Digest shortened to fit one Slack message; some authors or activity links are omitted.]';
124202
const maximumLength = 35000 - shortened.length;
125203
const lines = nonEmployees.flatMap((author) => [
126204
`\n\n@${author.login} (https://github.com/${author.login})\n` +
127-
`Comments: ${author.comments.length}; conversations: ` +
128-
`${new Set(author.comments.map((comment) => comment.conversation)).size}`,
129-
...author.comments.map((comment) => `\n ${comment.url}`),
205+
`Comments: ${author.activities.filter((activity) => activity.kind === 'comment').length}; ` +
206+
`reviews: ${author.activities.filter((activity) => activity.kind === 'review').length}; conversations: ` +
207+
`${new Set(author.activities.map((activity) => activity.conversation)).size}`,
208+
...author.activities.map((activity) =>
209+
`\n ${activity.kind === 'review' ? 'Review: ' : ''}${activity.url}`),
130210
]);
131211
for (const line of lines) {
132212
if (message.length + line.length > maximumLength) {

0 commit comments

Comments
 (0)