From 9841c311ddc01ba85b40f00f6577e979ba167a69 Mon Sep 17 00:00:00 2001 From: Dominic Saadi <32992335+jtoar@users.noreply.github.com> Date: Wed, 19 Jan 2022 17:43:51 -0800 Subject: [PATCH] update release notes script (#4212) --- tasks/release-notes/release-notes.mjs | 186 +++++++++++++++++--------- 1 file changed, 126 insertions(+), 60 deletions(-) diff --git a/tasks/release-notes/release-notes.mjs b/tasks/release-notes/release-notes.mjs index a1d832ea5902..0de69c3942f9 100755 --- a/tasks/release-notes/release-notes.mjs +++ b/tasks/release-notes/release-notes.mjs @@ -9,15 +9,15 @@ import yargs from 'yargs' import { hideBin } from 'yargs/helpers' /** - * If the user didn't set a GitHub token, exit early. + * If the user didn't provide a GitHub token, exit early. */ if (!process.env.GITHUB_TOKEN) { console.log() console.error( - ` You have to provide a github token. Make sure there's a var named GITHUB_TOKEN in your env.` + ` You have to provide a GitHub personal-access token (PAT) by setting it to an env var named "GITHUB_TOKEN"` ) console.error( - ` You can provision an personal access token here: https://github.com/settings/tokens` + ` You can provision a PAT here: https://github.com/settings/tokens` ) console.log() @@ -44,29 +44,97 @@ function builder(yargs) { .example('$0 v0.40.0', 'Build release notes for v0.40.0') } -const milestonesQuery = ` - query($title: String) { +const GET_MILESTONE_IDS = ` + query GetMilestoneIds($title: String) { repository(owner: "redwoodjs", name: "redwood") { - milestones(query: $title, first: 3, orderBy: { field: NUMBER, direction: DESC }) { + milestones( + query: $title + first: 100 + orderBy: { field: NUMBER, direction: DESC } + ) { nodes { title id - pullRequests(first: 100) { - nodes { - number - title - author { - login + } + } + } + } +` + +/** + * @param {string} title + */ +async function getMilestoneId(title) { + const { + repository: { milestones }, + } = await octokit.graphql(GET_MILESTONE_IDS, { title }) + + let milestone = milestones.nodes.find( + (milestone) => milestone.title === title + ) + + if (!milestone) { + const [latestMilestone] = milestones.nodes + console.log( + `No milestone was provided; using the latest: ${latestMilestone.title}` + ) + milestone = latestMilestone + } + + return milestone +} + +const GET_PRS_WITH_MILESTONE = ` + query GetPRsWithMilestone($milestoneId: ID!, $after: String) { + node(id: $milestoneId) { + ... on Milestone { + pullRequests(first: 100, after: $after) { + pageInfo { + hasNextPage + endCursor + } + nodes { + number + title + author { + login + } + labels(first: 100) { + nodes { + name } } - totalCount } + totalCount } } } } ` +/** + * @param {{ milestoneId: string, after?: string }} + */ +async function getPRsWithMilestone({ milestoneId, after }) { + const { + node: { pullRequests }, + } = await octokit.graphql(GET_PRS_WITH_MILESTONE, { + milestoneId, + after, + }) + + if (!pullRequests.pageInfo.hasNextPage) { + return pullRequests.nodes + } + + const prs = await getPRsWithMilestone({ + milestoneId, + after: pullRequests.pageInfo.endCursor, + }) + + return [...pullRequests.nodes, ...prs] +} + /** * This function does pretty much all the work. * @@ -78,56 +146,19 @@ async function handler(argv) { /** * Get the milestone's title, id, and PRs. */ - let title = argv.milestone - let prs - - const { - repository: { milestones }, - } = await octokit.graphql(milestonesQuery, { title }) - - /** - * If no milestone was provided, use the latest. - */ - if (title === undefined) { - console.log(milestones.nodes) - const [latestMilestone] = milestones.nodes - - title = latestMilestone.title - prs = latestMilestone.pullRequests.nodes - } else { - const milestone = milestones.nodes.find( - (milestone) => milestone.title === title - ) - - prs = milestone.pullRequests.nodes - } - - /** - * Interpolate the template and write to `${cwd}/${milestone}-release-notes.md`. - * - * @see {@link https://nodejs.org/docs/latest-v15.x/api/esm.html#esm_no_filename_or_dirname} - */ - const interpolate = template( - fs.readFileSync( - new URL('release-notes.md.template', import.meta.url), - 'utf8' - ) - ) + const { title, id } = await getMilestoneId(argv.milestone) + const prs = await getPRsWithMilestone({ milestoneId: id }) + const filename = new URL(`${title}-release-notes.md`, import.meta.url) const filedata = interpolate({ uniqueContributors: getNoOfUniqueContributors(prs), - prsMerged: prs.length, + prsMerged: prs.filter((pr) => pr.author.login !== 'renovate').length, ...sortPRs(prs), }) - - const filename = new URL(`${title}-release-notes.md`, import.meta.url) - fs.writeFileSync(filename, filedata) - console.log() console.log(`Written to ${url.fileURLToPath(filename)}`) console.log('Done') - console.log() } yargs(hideBin(process.argv)) @@ -145,6 +176,15 @@ yargs(hideBin(process.argv)) * Helper functions. */ +/** + * Interpolate the template and write to `${cwd}/${milestone}-release-notes.md`. + * + * @see {@link https://nodejs.org/docs/latest-v15.x/api/esm.html#esm_no_filename_or_dirname} + */ +const interpolate = template( + fs.readFileSync(new URL('release-notes.md.template', import.meta.url), 'utf8') +) + /** * A helper function for formatting PRs. * A `pr` looks like: @@ -172,19 +212,19 @@ function formatPR(pr) { } function getNoOfUniqueContributors(prs) { - const logins = prs.map((pr) => pr.author.login) + const logins = prs + .map((pr) => pr.author.login) + .filter((login) => login !== 'renovate') + return new Set(logins).size } /** - * This is just a stub till we have some kind of changesets integration. - * * @param {Array<{ * number: number, * title: string, - * author: { - * login: string, - * } + * author: { login: string } + * labels: { nodes: Array<{ name: string }> } * }>} prs */ function sortPRs(prs) { @@ -195,11 +235,37 @@ function sortPRs(prs) { const manual = [] for (const pr of prs) { + /** + * Sort `packageDependencies` by author (i.e. renovate bot). + */ if (pr.author.login === 'renovate') { packageDependencies.push(`