Skip to content

Commit fb3f94b

Browse files
committed
chore(ci): add first centralized reusable workflow
1 parent ffd8e0d commit fb3f94b

File tree

2 files changed

+315
-0
lines changed

2 files changed

+315
-0
lines changed

Diff for: playground/.prettierrc

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"tabWidth": 2,
3+
"useTabs": false
4+
}

Diff for: playground/app.mjs

+311
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
const DEFAULT_EMPTY_RESPONSE = [{}];
2+
const MONTH = new Date().toLocaleString("default", { month: "long" });
3+
const BLOCKED_LABELS = [
4+
"do-not-merge",
5+
"need-issue",
6+
"need-rfc",
7+
"need-customer-feedback",
8+
];
9+
10+
/**
11+
* Calculate the difference in days between the current date and a given datetime.
12+
*
13+
* @param {string} datetime - The datetime string to calculate the difference from.
14+
* @returns {number} - The difference in days between the current date and the given datetime.
15+
*/
16+
const diffInDays = (datetime) => {
17+
const diff_in_ms = new Date() - new Date(datetime);
18+
19+
// ms(1000)->seconds(60)->minutes(60)->hours(24)->days
20+
return Math.floor(diff_in_ms / (1000 * 60 * 60 * 24));
21+
};
22+
23+
/**
24+
* Formats a datetime string into a localized human date string e.g., April 5, 2024.
25+
*
26+
* @param {string} datetime - The datetime string to format.
27+
* @returns {string} The formatted date string.
28+
*
29+
* @example
30+
* const datetime = "2022-01-01T12:00:00Z";
31+
* console.log(formatDate(datetime)); // April 5, 2024
32+
*/
33+
const formatDate = (datetime) => {
34+
const date = new Date(datetime);
35+
return new Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format(date);
36+
};
37+
38+
/**
39+
* Generates a markdown table from an array of key:value object.
40+
*
41+
* This function takes an array of objects as input and generates a markdown table with the keys as column headings and the values as rows.
42+
*
43+
* @param {Array<Object>} data - The data to generate the table from.
44+
* @returns {Object} An object containing the formatted table components.
45+
* - heading: The formatted column headings of the table.
46+
* - dashes: The formatted dashed line separating the headings from the rows.
47+
* - rows: The formatted rows of the table.
48+
*
49+
* @example
50+
* const data = [
51+
* { name: 'John', age: 30, city: 'New York' },
52+
* { name: 'Jane', age: 25, city: 'London' },
53+
* { name: 'Bob', age: 35, city: 'Paris' }
54+
* ];
55+
*
56+
* const table = buildMarkdownTable(data);
57+
* console.log(table.heading); // '| name | age | city |'
58+
* console.log(table.dashes); // '| ---- | --- | ---- |'
59+
* console.log(table.rows); // '| John | 30 | New York |'
60+
*/
61+
const buildMarkdownTable = (data) => {
62+
const keys = Object.keys(data[0]);
63+
64+
if (keys.length === 0) {
65+
return "Not available";
66+
}
67+
68+
const formatted_headings = `${keys.join(" | ")}`;
69+
const keyLength = keys[0]?.length || 0;
70+
const dashes = keys.map(() => `${"-".repeat(keyLength)} |`).join(" ");
71+
72+
const values = data.map((issues) => Object.values(issues));
73+
const rows = values.map((row) => `${row.join(" | ")} |`).join("\n");
74+
75+
return `${formatted_headings}
76+
${dashes}
77+
${rows}`;
78+
};
79+
80+
/**
81+
* Retrieves a list of PRs from a repository sorted by `reactions-+1` keyword.
82+
*
83+
* @param {import('@types/github-script').AsyncFunctionArguments} AsyncFunctionArguments
84+
* @typedef {Object} Response
85+
* @property {string} title - The title of the issue, with a link to the issue.
86+
* @property {string} created_at - The creation date of the issue, formatted as April 5, 2024.
87+
* @property {number} reaction_count - The total number of reactions on the issue.
88+
* @property {string} labels - The labels of the issue, enclosed in backticks.
89+
* @returns {Promise<Array<Response>>} A promise resolving with an array of issue objects.
90+
*
91+
*/
92+
async function getTopFeatureRequests({ github, context, core }) {
93+
core.info("Fetching feature requests sorted by +1 reactions");
94+
95+
const { data: issues } = await github.rest.issues.listForRepo({
96+
owner: context.repo.owner,
97+
repo: context.repo.repo,
98+
labels: "feature-request",
99+
sort: "reactions-+1",
100+
direction: "desc",
101+
per_page: 3,
102+
});
103+
104+
core.info("Successfully fetched issues");
105+
core.debug(issues);
106+
107+
return issues.map((issue) => ({
108+
title: `[${issue.title}](${issue.html_url})`,
109+
created_at: formatDate(issue.created_at),
110+
reaction_count: issue.reactions.total_count,
111+
labels: `${issue.labels.map((label) => `\`${label.name}\``).join(",")}`, // enclose each label with `<label>` for rendering
112+
}));
113+
}
114+
115+
/**
116+
* Retrieves a list of issues from a repository sorted by `comments` keyword.
117+
*
118+
* @param {import('@types/github-script').AsyncFunctionArguments} AsyncFunctionArguments
119+
* @typedef {Object} Response
120+
* @property {string} title - The title of the issue, with a link to the issue.
121+
* @property {string} created_at - The creation date of the issue, formatted as April 5, 2024.
122+
* @property {number} comment_count - The total number of comments in the issue.
123+
* @property {string} labels - The labels of the issue, enclosed in backticks.
124+
* @returns {Promise<Array<Response>>} A promise resolving with an array of issue objects.
125+
*
126+
*/
127+
async function getTopMostCommented({ github, context, core }) {
128+
core.info("Fetching issues sorted by comments");
129+
130+
const { data: issues } = await github.rest.issues.listForRepo({
131+
owner: context.repo.owner,
132+
repo: context.repo.repo,
133+
sort: "comments",
134+
direction: "desc",
135+
per_page: 3,
136+
});
137+
138+
core.info("Successfully fetched issues");
139+
core.debug(issues);
140+
141+
return issues.map((issue) => ({
142+
title: `[${issue.title}](${issue.html_url})`,
143+
created_at: formatDate(issue.created_at),
144+
comment_count: issue.comments,
145+
labels: `${issue.labels.map((label) => `\`${label.name}\``).join(",")}`, // enclose each label with `<label>` for rendering
146+
}));
147+
}
148+
149+
/**
150+
* Retrieves a list of oldest issues from a repository sorted by `created` keyword, excluding blocked labels.
151+
*
152+
* @param {import('@types/github-script').AsyncFunctionArguments} AsyncFunctionArguments
153+
*
154+
* @typedef {Object} Response
155+
* @property {string} title - The title of the issue, with a link to the issue.
156+
* @property {string} created_at - The creation date of the issue, formatted as April 5, 2024.
157+
* @property {number} last_update - Number of days since the last update.
158+
* @property {string} labels - The labels of the issue, enclosed in backticks.
159+
* @returns {Promise<Array<Response>>} A promise resolving with an array of issue objects.
160+
*/
161+
async function getTopOldestIssues({ github, context, core }) {
162+
core.info("Fetching issues sorted by creation date");
163+
const { data: issues } = await githubClient.rest.issues.listForRepo({
164+
owner: context.repo.owner,
165+
repo: context.repo.repo,
166+
sort: "created",
167+
direction: "asc",
168+
per_page: 30,
169+
});
170+
171+
core.info("Successfully fetched issues");
172+
core.debug(issues);
173+
174+
core.info(
175+
`Filtering out issues that contained blocking labels: ${BLOCKED_LABELS}`
176+
);
177+
const top3 = issues
178+
.filter((issue) =>
179+
issue.labels.every((label) => !BLOCKED_LABELS.includes(label.name))
180+
)
181+
.slice(0, 3);
182+
183+
core.debug(top3);
184+
185+
return top3.map((issue) => {
186+
return {
187+
title: `[${issue.title}](${issue.html_url})`,
188+
created_at: formatDate(issue.created_at),
189+
last_update: `${diffInDays(issue.updated_at)} days`,
190+
labels: `${issue.labels.map((label) => `\`${label.name}\``).join(",")}`, // enclose each label with `<label>` for rendering
191+
};
192+
});
193+
}
194+
195+
/**
196+
* Retrieves a list of long running pull requests from a repository, excluding blocked labels.
197+
*
198+
* @param {import('@types/github-script').AsyncFunctionArguments} AsyncFunctionArguments
199+
*
200+
* @typedef {Object} Response
201+
* @property {string} title - The title of the PR, with a link to the PR.
202+
* @property {string} created_at - The creation date of the PR, formatted as April 5, 2024.
203+
* @property {number} last_update - Number of days since the last update.
204+
* @property {string} labels - The labels of the PR, enclosed in backticks.
205+
* @returns {Promise<Array<Response>>} A promise resolving with an array of PR objects.
206+
*/
207+
async function getLongRunningPRs({ github, context, core }) {
208+
core.info("Fetching PRs sorted by long-running");
209+
const { data: prs } = await github.rest.pulls.list({
210+
owner: context.repo.owner,
211+
repo: context.repo.repo,
212+
sort: "long-running",
213+
direction: "desc",
214+
per_page: 30,
215+
});
216+
217+
core.debug(issues);
218+
219+
core.info(
220+
`Filtering out issues that contained blocking labels: ${BLOCKED_LABELS}`
221+
);
222+
const top3 = prs
223+
.filter((pr) =>
224+
pr.labels.every((label) => !BLOCKED_LABELS.includes(label.name))
225+
)
226+
.slice(0, 3);
227+
228+
core.debug(top3);
229+
230+
return top3.map((issue) => {
231+
return {
232+
title: `[${issue.title}](${issue.html_url})`,
233+
created_at: formatDate(issue.created_at),
234+
last_update: `${diffInDays(issue.updated_at)} days`,
235+
labels: `${issue.labels.map((label) => `\`${label.name}\``).join(",")}`, // enclose each label with `<label>` for rendering
236+
};
237+
});
238+
}
239+
240+
/**
241+
* Creates a monthly roadmap issue report with top PFRs, most active issues, and stale requests.
242+
*
243+
* Example issue: https://github.com/heitorlessa/action-script-playground/issues/24
244+
*
245+
* @param {import('@types/github-script').AsyncFunctionArguments} AsyncFunctionArguments
246+
* @returns {Promise<void>} A promise resolving when the issue is created.
247+
*
248+
*/
249+
async function createMonthlyRoadmapReport({ github, context, core }) {
250+
core.info("Fetching GitHub data concurrently");
251+
252+
const [
253+
{ value: featureRequests = DEFAULT_EMPTY_RESPONSE },
254+
{ value: longRunningPRs = DEFAULT_EMPTY_RESPONSE },
255+
{ value: oldestIssues = DEFAULT_EMPTY_RESPONSE },
256+
{ value: mostActiveIssues = DEFAULT_EMPTY_RESPONSE },
257+
] = await Promise.allSettled([
258+
getTopFeatureRequests({ github, context, core }),
259+
getLongRunningPRs({ github, context, core }),
260+
getTopOldestIssues({ github, context, core }),
261+
getTopMostCommented({ github, context, core }),
262+
]);
263+
264+
const tables = {
265+
featureRequests: buildMarkdownTable(featureRequests),
266+
mostActiveIssues: buildMarkdownTable(mostActiveIssues),
267+
longRunningPRs: buildMarkdownTable(longRunningPRs),
268+
oldestIssues: buildMarkdownTable(oldestIssues),
269+
};
270+
271+
const body = `
272+
273+
Quick report of top 3 issues/PRs to assist in roadmap updates. Issues or PRs with the following labels are excluded:
274+
275+
* \`do-not-merge\`
276+
* \`need-rfc\`
277+
* \`need-issue\`
278+
* \`need-customer-feedback\`
279+
280+
> **NOTE**: It does not guarantee they will be in the roadmap. Some might already be and there might be a blocker.
281+
282+
## Top 3 Feature Requests
283+
284+
${tables.featureRequests}
285+
286+
## Top 3 Most Commented Issues
287+
288+
${tables.mostActiveIssues}
289+
290+
## Top 3 Long Running Pull Requests
291+
292+
${tables.longRunningPRs}
293+
294+
## Top 3 Oldest Issues
295+
296+
${tables.oldestIssues}
297+
`;
298+
299+
return await githubClient.issues.create({
300+
owner: context.repo.owner,
301+
repo: context.repo.repo,
302+
title: `Roadmap update reminder - ${MONTH}`,
303+
body,
304+
});
305+
}
306+
307+
// @ts-check
308+
/** @param {import('@types/github-script').AsyncFunctionArguments} AsyncFunctionArguments */
309+
module.exports = async ({ github, context, core }) => {
310+
return await createMonthlyRoadmapReport({ github, context, core });
311+
};

0 commit comments

Comments
 (0)