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 .
44on :
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