From 8b95fc76a926763540898f7fc9f74bf3f1feac80 Mon Sep 17 00:00:00 2001 From: Dominic Saadi <32992335+jtoar@users.noreply.github.com> Date: Thu, 27 Jan 2022 14:38:43 -0800 Subject: [PATCH] Add release tooling (#4274) * add release tooling * correct package json script * remove notes * remove comments * improve comments, disable some functionality * flesh out release patch * fix constraints * mv release-notes * rename release notes files * update yarn lock, many changes * updates * dedupe * update, add tests * tests * add msw, make more interactive * test updates * restore choices array * handle release notes * add confirm * improve prompts * prompts * copy * fix bug in prompt * prompts * updates * fix * updates Co-authored-by: David Price --- .gitignore | 2 +- package.json | 10 +- tasks/release-notes/README.md | 11 - tasks/release-notes/release-notes.md.template | 30 -- .../generateReleaseNotes.test.mjs.snap | 35 ++ tasks/release/cli.mjs | 37 ++ .../generateReleaseNotes.mjs} | 304 ++++++++-------- tasks/release/generateReleaseNotes.test.mjs | 189 ++++++++++ tasks/release/jest.config.mjs | 8 + tasks/release/jest.setup.mjs | 3 + tasks/release/octokit.mjs | 22 ++ tasks/release/release.mjs | 342 ++++++++++++++++++ ...updateNextReleasePullRequestsMilestone.mjs | 276 ++++++++++++++ ...eNextReleasePullRequestsMilestone.test.mjs | 237 ++++++++++++ yarn.lock | 126 ++++++- 15 files changed, 1425 insertions(+), 207 deletions(-) delete mode 100644 tasks/release-notes/README.md delete mode 100644 tasks/release-notes/release-notes.md.template create mode 100644 tasks/release/__snapshots__/generateReleaseNotes.test.mjs.snap create mode 100644 tasks/release/cli.mjs rename tasks/{release-notes/release-notes.mjs => release/generateReleaseNotes.mjs} (56%) create mode 100644 tasks/release/generateReleaseNotes.test.mjs create mode 100644 tasks/release/jest.config.mjs create mode 100644 tasks/release/jest.setup.mjs create mode 100644 tasks/release/octokit.mjs create mode 100644 tasks/release/release.mjs create mode 100644 tasks/release/updateNextReleasePullRequestsMilestone.mjs create mode 100644 tasks/release/updateNextReleasePullRequestsMilestone.test.mjs diff --git a/.gitignore b/.gitignore index 1acd5ea4252b..f942d04cb422 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ yarn-error.log **/*.tsbuildinfo tasks/.verdaccio tasks/e2e/cypress/fixtures/example.json -tasks/release-notes/*.md +tasks/release/*.md tmp/ blog-test-project/* .yarn/* diff --git a/package.json b/package.json index 321cae72b50a..f9c58fd8698e 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,13 @@ "@types/jest": "27.4.0", "@types/jscodeshift": "0.11.2", "@types/lodash.template": "4.5.0", + "@types/prompts": "2.4.0", "all-contributors-cli": "6.20.0", "ansi-colors": "4.1.1", "babel-jest": "27.4.6", "babel-plugin-auto-import": "1.1.0", "babel-plugin-remove-code": "0.0.6", + "boxen": "5.1.2", "core-js": "3.20.3", "cypress": "9.3.1", "cypress-wait-until": "1.7.2", @@ -36,14 +38,17 @@ "jscodeshift": "0.13.0", "lerna": "4.0.0", "lodash.template": "4.5.0", + "msw": "0.36.7", "nodemon": "2.0.15", "npm-packlist": "3.0.0", "octokit": "1.7.1", "ora": "5.4.1", + "prompts": "2.4.2", "rimraf": "3.0.2", "terminal-link": "2.1.1", "typescript": "4.5.5", - "typescript-transform-paths": "3.3.1" + "typescript-transform-paths": "3.3.1", + "zx": "4.3.0" }, "resolutions": { "@types/react": "17.0.38", @@ -63,13 +68,14 @@ "build:clean": "yarn clean:prisma && rimraf packages/**/dist", "build:watch": "lerna run build:watch --parallel; tsc --build", "test": "lerna run test --stream -- --colors --maxWorkers=4", + "test-release": "NODE_OPTIONS=--experimental-vm-modules ./node_modules/.bin/jest --config ./tasks/release/jest.config.mjs", "e2e": "node ./tasks/run-e2e", "clean:prisma": "rimraf node_modules/.prisma/client && node node_modules/@prisma/client/scripts/postinstall.js", "lint": "RWJS_CWD=packages/create-redwood-app/template eslint --config .eslintrc.js packages", "lint:fix": "yarn lint --fix", "build:link": "node ./tasks/build-and-copy", "build:test-project": "node ./tasks/test-project/test-project", - "release-notes": "node ./tasks/release-notes/release-notes.mjs", + "release": "node ./tasks/release/cli.mjs", "publish:canary": "lerna publish --force-publish --canary --include-merged-tags --preid canary --dist-tag canary --yes --loglevel verbose", "project:deps": "node ./tasks/framework-tools/frameworkDepsToProject.mjs", "project:copy": "node ./tasks/framework-tools/frameworkFilesToProject.mjs", diff --git a/tasks/release-notes/README.md b/tasks/release-notes/README.md deleted file mode 100644 index 19e4b3dfb270..000000000000 --- a/tasks/release-notes/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Release Notes - -This script builds release notes for a milestone: - -``` -yarn release-notes v0.34.0 -# There should now be a new file at ./tasks/release-notes/v0.34.0-release-notes.md -``` - -To run this script, you'll need a personal access token. -Provision one from https://github.com/settings/tokens and set it to `GITHUB_TOKEN` in your env. diff --git a/tasks/release-notes/release-notes.md.template b/tasks/release-notes/release-notes.md.template deleted file mode 100644 index 5d0100a916ef..000000000000 --- a/tasks/release-notes/release-notes.md.template +++ /dev/null @@ -1,30 +0,0 @@ -# Changelog - -Unique contributors: ${uniqueContributors} - -PRs merged: ${prsMerged} - -## Features - -${features} - -## Fixed - -${fixed} - -## Chore - -${chore} - -### Package Dependencies - -
-View all Dependency Version Upgrades - -
- -## Manual - -${manual} diff --git a/tasks/release/__snapshots__/generateReleaseNotes.test.mjs.snap b/tasks/release/__snapshots__/generateReleaseNotes.test.mjs.snap new file mode 100644 index 000000000000..cb734e370294 --- /dev/null +++ b/tasks/release/__snapshots__/generateReleaseNotes.test.mjs.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`generateReleaseNotes works 1`] = ` +"# Changelog + +Unique contributors: 3 + +PRs merged: 3 + +## Features + +- Added Decimal field type to Scaffold #4169 by @BBurnworth + +## Fixed + +- Type fix for \`mockGraphQL\` data argument #4164 by @callingmedic911 + +## Chore + +- Add storybook ci option to test that Storybook starts \\"ok\\" #3515 by @virtuoushub + +### Package Dependencies + +
+View all Dependency Version Upgrades + +
+ +## Manual + + +" +`; diff --git a/tasks/release/cli.mjs b/tasks/release/cli.mjs new file mode 100644 index 000000000000..2c235867dbf8 --- /dev/null +++ b/tasks/release/cli.mjs @@ -0,0 +1,37 @@ +#!/usr/bin/env node +/* eslint-env node, es2021 */ + +import yargs from 'yargs' +import { hideBin } from 'yargs/helpers' + +import generateReleaseNotes from './generateReleaseNotes.mjs' +import release from './release.mjs' +import updateNextReleasePullRequestsMilestone from './updateNextReleasePullRequestsMilestone.mjs' + +yargs(hideBin(process.argv)) + .scriptName('release') + .command('$0', 'Release RedwoodJS', {}, release) + .command( + 'generate-release-notes [milestone]', + 'Generates release notes for a given milestone', + (yargs) => { + yargs.positional('milestone', { + describe: 'The milestone to generate release notes for', + type: 'string', + }) + }, + (argv) => generateReleaseNotes(argv.milestone) + ) + .command( + 'update-next-release-prs-milestone ', + "Update next-release PRs' milestone. Note that this creates the milestone if it doesn't exist", + (yargs) => { + yargs.positional('milestone', { + describe: 'The milestone to update next-release PRs to', + type: 'string', + }) + }, + (argv) => updateNextReleasePullRequestsMilestone(argv.milestone) + ) + .help() + .parse() diff --git a/tasks/release-notes/release-notes.mjs b/tasks/release/generateReleaseNotes.mjs similarity index 56% rename from tasks/release-notes/release-notes.mjs rename to tasks/release/generateReleaseNotes.mjs index 8d54478c028a..efdbbb66ae77 100755 --- a/tasks/release-notes/release-notes.mjs +++ b/tasks/release/generateReleaseNotes.mjs @@ -1,55 +1,78 @@ -#!/usr/bin/env node -/* eslint-env node, es6*/ +/* eslint-env node, es2021 */ import template from 'lodash.template' import fs from 'node:fs' import url from 'node:url' -import { Octokit } from 'octokit' -import yargs from 'yargs' -import { hideBin } from 'yargs/helpers' + +import octokit from './octokit.mjs' /** - * If the user didn't provide a GitHub token, exit early. + * Generates release notes for a milestone. + * + * @remarks + * + * If no milestone's given, just fetch the latest version milestone (e.g. `v0.42.0`). + * + * @param {string} [milestone] */ -if (!process.env.GITHUB_TOKEN) { - console.log() - console.error( - ` 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 a PAT here: https://github.com/settings/tokens` - ) - console.log() +export default async function generateReleaseNotes(milestone) { + // Get the milestone's title, id, and PRs. + const { title, id } = await getMilestoneId(milestone) + const prs = await getPRsWithMilestone({ milestoneId: id }) + + const filename = new URL(`${title}ReleaseNotes.md`, import.meta.url) + const filedata = interpolate({ + uniqueContributors: getNoOfUniqueContributors(prs), + prsMerged: prs.filter((pr) => pr.author.login !== 'renovate').length, + ...sortPRs(prs), + }) + fs.writeFileSync(filename, filedata) - /** - * There's a few ways we could exit, - * but `process.exit` actually isn't recommended: {@link https://nodejs.dev/learn/how-to-exit-from-a-nodejs-program}. - */ - process.kill(process.pid) + console.log(`Written to ${url.fileURLToPath(filename)}`) + console.log('Done') } -const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }) +// Helpers /** - * Yargs bindings. + * @typedef {{ + * repository: { + * milestones: { + * nodes: Array<{ title: string, id: string }> + * } + * } + * }} GetMilestoneIdsRes * - * @param {typeof yargs} yargs + * @param {string} [title] */ -function builder(yargs) { - yargs - .positional('milestone', { - describe: 'The milestone to generate release notes for', - type: 'string', - }) - .example('$0 v0.40.0', 'Build release notes for v0.40.0') +async function getMilestoneId(title) { + const { + repository: { + milestones: { nodes: milestones }, + }, + } = /** @type GetMilestoneIdsRes */ ( + await octokit.graphql(GET_MILESTONE_IDS, { title }) + ) + + if (!title) { + const [latestMilestone] = milestones + console.log( + `No milestone was provided; using the latest: ${latestMilestone.title}` + ) + return latestMilestone + } + + let milestone = milestones.find((milestone) => milestone.title === title) + + return milestone } -const GET_MILESTONE_IDS = ` +export const GET_MILESTONE_IDS = ` query GetMilestoneIds($title: String) { repository(owner: "redwoodjs", name: "redwood") { milestones( query: $title - first: 100 + first: 3 orderBy: { field: NUMBER, direction: DESC } ) { nodes { @@ -62,29 +85,58 @@ const GET_MILESTONE_IDS = ` ` /** - * @param {string} title + * @typedef {{ + * number: number + * title: string + * author: { + * login: string + * }; + * labels: { + * nodes: Array<{ + * name: string + * }> + * } + * }} PR + * + * @typedef {{ + * node: { + * pullRequests: { + * pageInfo: { + * hasNextPage: boolean + * endCursor: string + * } + * nodes: Array + * totalCount: number + * } + * } + * }} GetPRsWithMilestoneRes + * + * @param {{ milestoneId: string, after?: string }} + * @returns {Promise>} */ -async function getMilestoneId(title) { +async function getPRsWithMilestone({ milestoneId, after }) { const { - repository: { milestones }, - } = await octokit.graphql(GET_MILESTONE_IDS, { title }) - - let milestone = milestones.nodes.find( - (milestone) => milestone.title === title + node: { pullRequests }, + } = /** @type GetPRsWithMilestoneRes */ ( + await octokit.graphql(GET_PRS_WITH_MILESTONE, { + milestoneId, + after, + }) ) - if (!milestone) { - const [latestMilestone] = milestones.nodes - console.log( - `No milestone was provided; using the latest: ${latestMilestone.title}` - ) - milestone = latestMilestone + if (!pullRequests.pageInfo.hasNextPage) { + return pullRequests.nodes } - return milestone + const prs = await getPRsWithMilestone({ + milestoneId, + after: pullRequests.pageInfo.endCursor, + }) + + return [...pullRequests.nodes, ...prs] } -const GET_PRS_WITH_MILESTONE = ` +export const GET_PRS_WITH_MILESTONE = ` query GetPRsWithMilestone($milestoneId: ID!, $after: String) { node(id: $milestoneId) { ... on Milestone { @@ -99,7 +151,7 @@ const GET_PRS_WITH_MILESTONE = ` author { login } - labels(first: 100) { + labels(first: 10) { nodes { name } @@ -113,104 +165,10 @@ const GET_PRS_WITH_MILESTONE = ` ` /** - * @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. - * - * @param {{ - * milestone: string, - * }} argv - */ -async function handler(argv) { - /** - * Get the milestone's title, id, and PRs. - */ - 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.filter((pr) => pr.author.login !== 'renovate').length, - ...sortPRs(prs), - }) - fs.writeFileSync(filename, filedata) - - console.log(`Written to ${url.fileURLToPath(filename)}`) - console.log('Done') -} - -yargs(hideBin(process.argv)) - .scriptName('release-notes') - .usage( - '$0 [milestone]', - 'Build release notes for a milestone', - builder, - handler - ) - .help() - .parse() - -/** - * 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: + * Get the number of unique contributors, excluding renovate bot. * - * ```js - * { - * "number": 2613, - * "title": "Scaffold Generator File Organization", - * "author": { - * "login": "cjreimer", - * }, - * } - * ``` - * - * @param {{ - * number: number, - * title: string, - * author: { - * login: string, - * } - * }} pr + * @param {Array} prs */ -function formatPR(pr) { - return `${pr.title} #${pr.number} by @${pr.author.login}` -} - function getNoOfUniqueContributors(prs) { const logins = prs .map((pr) => pr.author.login) @@ -220,12 +178,11 @@ function getNoOfUniqueContributors(prs) { } /** - * @param {Array<{ - * number: number, - * title: string, - * author: { login: string } - * labels: { nodes: Array<{ name: string }> } - * }>} prs + * @remarks + * + * This could be a little better. + * + * @param {Array} prs */ function sortPRs(prs) { const features = [] @@ -277,3 +234,50 @@ function sortPRs(prs) { manual: manual.join('\n'), } } + +/** + * @param {Array} pr + */ +function formatPR(pr) { + return `${pr.title} #${pr.number} by @${pr.author.login}` +} + +/** + * Interpolate the template and write to `${cwd}/${milestone}-releaseNotes.md`. + * + * @see {@link https://nodejs.org/docs/latest-v15.x/api/esm.html#esm_no_filename_or_dirname} + */ +const interpolate = template( + [ + '# Changelog', + '', + 'Unique contributors: ${uniqueContributors}', + '', + 'PRs merged: ${prsMerged}', + '', + '## Features', + '', + '${features}', + '', + '## Fixed', + '', + '${fixed}', + '', + '## Chore', + '', + '${chore}', + '', + '### Package Dependencies', + '', + '
', + 'View all Dependency Version Upgrades', + '
    ', + '${packageDependencies}', + '
', + '
', + '', + '## Manual', + '', + '${manual}', + ].join('\n') +) diff --git a/tasks/release/generateReleaseNotes.test.mjs b/tasks/release/generateReleaseNotes.test.mjs new file mode 100644 index 000000000000..f2f024b6d712 --- /dev/null +++ b/tasks/release/generateReleaseNotes.test.mjs @@ -0,0 +1,189 @@ +import { graphql } from 'msw' +import { setupServer } from 'msw/node' +import fs from 'node:fs' + +import generateReleaseNotes, { + GET_MILESTONE_IDS, + GET_PRS_WITH_MILESTONE, +} from './generateReleaseNotes.mjs' + +const handleGetMilestoneIds = graphql.query( + 'GetMilestoneIds', + (req, res, ctx) => { + const { title } = req.variables + + const payload = { + repository: { + milestones: { + nodes: [ + title + ? { + title, + id: `123-${title}`, + } + : { + title: 'v0.42.1', + id: 'MI_kwDOC2M2f84Ac_Ij', + }, + ], + }, + }, + } + + return res(ctx.data(payload)) + } +) + +const handleGetPRsWithMilestone = graphql.query( + 'GetPRsWithMilestone', + (_req, res, ctx) => { + const payload = { + node: { + pullRequests: { + pageInfo: { + hasNextPage: false, + endCursor: 'Y3Vyc29yOnYyOpLAzjEcbpg=', + }, + nodes: [ + { + number: 3515, + title: + 'Add storybook ci option to test that Storybook starts "ok"', + author: { + login: 'virtuoushub', + }, + labels: { + nodes: [ + { + name: 'topic/storybook', + }, + { + name: 'v1/priority', + }, + { + name: 'release:chore', + }, + ], + }, + }, + { + number: 4164, + title: 'Type fix for `mockGraphQL` data argument', + author: { + login: 'callingmedic911', + }, + labels: { + nodes: [ + { + name: 'topic/testing', + }, + { + name: 'v1/priority', + }, + { + name: 'release:fix', + }, + ], + }, + }, + { + number: 4169, + title: 'Added Decimal field type to Scaffold', + author: { + login: 'BBurnworth', + }, + labels: { + nodes: [ + { + name: 'release:feature', + }, + ], + }, + }, + ], + totalCount: 46, + }, + }, + } + + return res(ctx.data(payload)) + } +) + +const server = setupServer(handleGetMilestoneIds, handleGetPRsWithMilestone) + +beforeAll(() => server.listen()) + +afterAll(() => { + server.close() + + if ( + fs.existsSync(new URL('./next-releaseReleaseNotes.md', import.meta.url)) + ) { + fs.rmSync(new URL('./next-releaseReleaseNotes.md', import.meta.url)) + } +}) + +describe('generateReleaseNotes', () => { + it('uses the right queries', () => { + expect(GET_MILESTONE_IDS).toMatchInlineSnapshot(` + " + query GetMilestoneIds($title: String) { + repository(owner: \\"redwoodjs\\", name: \\"redwood\\") { + milestones( + query: $title + first: 3 + orderBy: { field: NUMBER, direction: DESC } + ) { + nodes { + title + id + } + } + } + } + " + `) + + expect(GET_PRS_WITH_MILESTONE).toMatchInlineSnapshot(` + " + 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: 10) { + nodes { + name + } + } + } + totalCount + } + } + } + } + " + `) + }) + + it('MSW unit test', async () => { + await generateReleaseNotes('next-release') + + expect( + fs.readFileSync( + new URL('./next-releaseReleaseNotes.md', import.meta.url), + 'utf8' + ) + ).toMatchSnapshot() + }) +}) diff --git a/tasks/release/jest.config.mjs b/tasks/release/jest.config.mjs new file mode 100644 index 000000000000..358dc958c501 --- /dev/null +++ b/tasks/release/jest.config.mjs @@ -0,0 +1,8 @@ +export default { + verbose: true, + setupFiles: ['/jest.setup.mjs'], + // ESM-specific settings: + transform: {}, + moduleFileExtensions: ['js', 'mjs'], + testMatch: ['/*.test.mjs'], +} diff --git a/tasks/release/jest.setup.mjs b/tasks/release/jest.setup.mjs new file mode 100644 index 000000000000..72231ee8e9d7 --- /dev/null +++ b/tasks/release/jest.setup.mjs @@ -0,0 +1,3 @@ +/* eslint-env node, es2021 */ + +process.env.GITHUB_TOKEN = 'github-token' diff --git a/tasks/release/octokit.mjs b/tasks/release/octokit.mjs new file mode 100644 index 000000000000..0bedbb6e23be --- /dev/null +++ b/tasks/release/octokit.mjs @@ -0,0 +1,22 @@ +/* eslint-env node, es2021 */ + +import { Octokit } from 'octokit' + +/** + * Exit with a helpful error message if the user didn't provide a GitHub token. + */ +if (!process.env.GITHUB_TOKEN) { + console.log() + console.error( + ` 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 a PAT here: https://github.com/settings/tokens` + ) + console.log() + process.exit(1) +} + +const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }) + +export default octokit diff --git a/tasks/release/release.mjs b/tasks/release/release.mjs new file mode 100644 index 000000000000..c1a314b0d401 --- /dev/null +++ b/tasks/release/release.mjs @@ -0,0 +1,342 @@ +/* eslint-env node, es2021 */ +/** + * Notes + * - branch already exists? start on the "update package versions" step + * - consider using xstate + */ +import c from 'ansi-colors' +import boxen from 'boxen' +import prompts from 'prompts' +import { $ } from 'zx' + +import generateReleaseNotes from './generateReleaseNotes.mjs' +import updateNextReleasePullRequestsMilestone, { + closeMilestone, +} from './updateNextReleasePullRequestsMilestone.mjs' + +export const ASK = c.bgBlue(c.black(' ASK ')) +export const CHECK = c.bgYellow(c.black(' CHECK ')) +export const FIX = c.bgRed(c.black(' FIX ')) + +export default async function release() { + const { semver } = await exitOnCancelPrompts({ + type: 'select', + name: 'semver', + message: `${ASK} Which semver are you releasing?`, + choices: [{ value: 'major' }, { value: 'minor' }, { value: 'patch' }], + initial: 2, + }) + + // Get the most-recent tag and get the next version from it. + // `git describe --abbrev=0` should output something like like `v0.42.1`. + const gitDescribePO = await $`git describe --abbrev=0` + const previousVersion = gitDescribePO.stdout.trim() + let nextVersion = getNextVersion(semver, previousVersion) + + // Confirm that we got the next version right; give the user a chance to correct it if we didn't. + const nextVersionConfirmed = await confirm( + `${CHECK} The next release is ${c.green(nextVersion)}` + ) + if (!nextVersionConfirmed) { + const answer = await exitOnCancelPrompts({ + type: 'text', + name: 'nextVersion', + message: `${ASK} enter the next version`, + }) + nextVersion = answer.nextVersion + } + + // Check that the git tag doesn't already exist. + const gitTagPO = await $`git tag -l ${nextVersion}` + if (gitTagPO.stdout.trim()) { + console.log( + c.bold( + `${c.red('\u2716')} ${FIX} Git tag ${c.green( + nextVersion + )} already exists locally. You must resolve this before proceeding` + ) + ) + return + } + + const shouldUpdateNextReleasePRsMilestone = await confirm( + `${ASK} Do you want to update next-release PRs' milestone to ${c.green( + nextVersion + )}?` + ) + + let milestone + + if (shouldUpdateNextReleasePRsMilestone) { + try { + milestone = await updateNextReleasePullRequestsMilestone(nextVersion) + } catch (e) { + console.log( + `Couldn't update next-release PRs milestone to ${nextVersion}` + ) + console.log(e) + } + } + + switch (semver) { + case 'major': + { + console.log(c.yellow('Wait till after v1!')) + } + break + case 'minor': + await releaseMinor(nextVersion) + break + case 'patch': + await releasePatch(previousVersion, nextVersion) + break + } + + console.log(rocketBoxen(`Released ${c.green(nextVersion)}`)) + + const shouldGenerateReleaseNotes = await confirm( + `${ASK} Do you want to generate release notes?` + ) + if (shouldGenerateReleaseNotes) { + try { + await generateReleaseNotes(nextVersion) + } catch (e) { + console.log("Couldn't generate release notes") + console.log(e) + } + } + + if (milestone) { + const okToClose = await confirm( + `${ASK} Ok to close milestone ${c.green(nextVersion)}?` + ) + if (okToClose) { + closeMilestone(milestone.number) + } + } +} + +// Helpers + +/** + * Take the output from `git describe --abbrev=0` (which is something like `'v0.42.1'`), + * and return an array of numbers ([0, 42, 1]). + * + * @param {string} version the version string (obtain by running `git describe --abbrev=0`) + * @returns [string, string, string] + */ +function parseGitTag(version) { + if (version.startsWith('v')) { + version = version.substring(1) + } + + return version.split('.').map(Number) +} + +/** + * Bump the version according to the semver we're releasing. + * + * @typedef {'major' | 'minor' | 'patch'} Semver + * @param {Semver} semver + * @param {string} previousVersion + */ +function getNextVersion(semver, previousVersion) { + switch (semver) { + case 'major': { + const [major] = parseGitTag(previousVersion) + return `v${[major + 1, 0, 0].join('.')}` + } + case 'minor': { + const [major, minor] = parseGitTag(previousVersion) + return `v${[major, minor + 1, 0].join('.')}` + } + case 'patch': { + const [major, minor, patch] = parseGitTag(previousVersion) + return `v${[major, minor, patch + 1].join('.')}` + } + } +} + +/** + * Wrapper around `prompts` to exit on crtl c. + * + * @template Name + * @param {import('prompts').PromptObject} promptsObject + * @param {import('prompts').Options} promptsOptions + */ +function exitOnCancelPrompts(promptsObject, promptsOptions) { + return prompts(promptsObject, { + ...promptsOptions, + onCancel: () => process.exit(1), + }) +} + +/** + * Wrapper around confirm type `prompts`. + * + * @param {string} message + * @returns {Promise} + */ +export async function confirm(message) { + const answer = await exitOnCancelPrompts({ + type: 'confirm', + name: 'confirm', + message, + }) + + return answer.confirm +} + +/** + * Right now releasing a major is the same as releasing a minor. + * + * @param {string} nextVersion + */ +// function releaseMajor(nextVersion) { +// return releaseMajorOrMinor('major', nextVersion) +// } + +/** + * @param {string} nextVersion + */ +function releaseMinor(nextVersion) { + return releaseMajorOrMinor('minor', nextVersion) +} + +/** + * @param {Semver} semver + * @param {string} nextVersion + */ +async function releaseMajorOrMinor(semver, nextVersion) { + const PO = await $`git branch --show-current` + const branch = PO.stdout.trim() + if (branch !== 'main') { + console.log('Not on main. Checking out main') + await $`git checkout main` + } + + const releaseBranch = ['release', semver, nextVersion].join('/') + const okToCheckout = await confirm( + `${ASK} Ok to checkout new branch ${c.green(releaseBranch)}?` + ) + if (!okToCheckout) { + return + } + await $`git checkout -b ${releaseBranch}` + + const okToProceed = await confirm( + `${ASK} Checked out new release branch ${c.green( + releaseBranch + )}.\nIf you want to continue publishing, proceed.\nOtherwise, stop here to publish this branch to GitHub to create an RC` + ) + if (!okToProceed) { + return + } + + const okToCleanInstallUpdate = await confirm( + `${ASK} Ok to clean, install, and update package versions?` + ) + if (!okToCleanInstallUpdate) { + return + } + + await $`git clean -fxd` + await $`yarn install` + await $`./tasks/update-package-versions ${nextVersion}` + const versionsLookRight = await confirm( + `${CHECK} The package versions have been updated. Does everything look right?` + ) + if (!versionsLookRight) { + return + } + + const commitTagQA = await confirm( + `${ASK} Ok to commit, tag, and run through local QA?` + ) + if (!commitTagQA) { + return + } + + await $`git commit -am "${nextVersion}"` + await $`git tag -am ${nextVersion} "${nextVersion}"` + // QA + await $`yarn build` + await $`yarn lint` + await $`yarn test` + + const okToRelease = await confirm( + `${ASK} Everything passed local QA. Are you ready to push your branch to GitHub and publish to NPM?` + ) + if (!okToRelease) { + return + } + // await $`git push && git push --tags` + // await $`yarn lerna publish from-package` +} + +/** + * This is a WIP. + * + * @param {string} nextVersion + */ +async function releasePatch(previousVersion, nextVersion) { + await $`git checkout tags/${previousVersion} -b release/patch/${nextVersion}` + + await $`git push origin release/patch/${nextVersion}` + await `open https://github.com/redwoodjs/redwood/compare/${previousVersion}..release/patch/${nextVersion}` + + const diffLooksGood = await confirm('Does the diff look good?') + + if (!diffLooksGood) { + console.log('Resetting...') + } + + const proceed = await confirm('Cherry pick and handle the conflicts') + + if (!proceed) { + console.log('Resetting...') + } + + await $`git push origin release/patch/${nextVersion}` + await `open https://github.com/redwoodjs/redwood/compare/${previousVersion}..release/patch/${nextVersion}` + + const diffLooksGood2 = await confirm('Does the diff look good?') + + if (!diffLooksGood2) { + console.log('Resetting...') + } + + // publish rc + + await $`git clean -fxd` + await $`yarn install` + + await $`./tasks/update-package-versions ${nextVersion}` + + await $`yarn build` + + await $`git commit -am "${nextVersion}"` + await $`git tag -am ${nextVersion} "${nextVersion}"` + + await $`git checkout main` + // merge commit + await $`git branch -d release/patch/${nextVersion}` +} + +/** + * @param {string} message + */ +function rocketBoxen(message) { + boxen(message, { + padding: 1, + margin: 1, + borderStyle: { + bottomLeft: '🚀', + bottomRight: '🚀', + horizontal: '—', + topLeft: '🚀', + topRight: '🚀', + vertical: '🚀', + }, + }) +} diff --git a/tasks/release/updateNextReleasePullRequestsMilestone.mjs b/tasks/release/updateNextReleasePullRequestsMilestone.mjs new file mode 100644 index 000000000000..5687d983e58a --- /dev/null +++ b/tasks/release/updateNextReleasePullRequestsMilestone.mjs @@ -0,0 +1,276 @@ +/* eslint-env node, es2021 */ +/** + * - is:pr is:merged no:milestone -> should empty + * - milestone:next-release-patch -> check empty + * - close milestone _after_ publish + */ +import c from 'ansi-colors' + +import octokit from './octokit.mjs' +import { confirm, ASK, CHECK } from './release.mjs' + +/** + * @param {string} title + */ +export default async function updateNextReleasePullRequestsMilestone(title) { + let milestone = await getMilestone(title) + + if (!milestone) { + const okToCreate = await confirm( + `${ASK} Milestone ${c.green(title)} doesn't exist. Ok to create it?` + ) + + if (!okToCreate) { + return + } + + const { + data: { node_id: id, number }, + } = await createMilestone(title) + + milestone = { title, id, number } + + console.log(`Created milestone ${c.green(title)}`) + } + + const nextReleaseMilestoneId = await getNextReleaseMilestoneId() + + const pullRequestIds = await getPullRequestIdsWithMilestone( + nextReleaseMilestoneId + ) + + const okToUpdate = await confirm( + `${ASK} Ok to update the milestone of ${ + pullRequestIds.length + } PRs from ${c.green('next-release')} to ${c.green(title)}?` + ) + + if (!okToUpdate) { + return + } + + await Promise.all( + pullRequestIds.map((pullRequestId) => + updatePullRequestMilestone(pullRequestId, milestone.id) + ) + ) + + const looksRight = await confirm( + `${CHECK} Updated the milestone of ${pullRequestIds.length} PRs: + https://github.com/redwoodjs/redwood/pulls?q=is%3Apr+is%3Amerged+milestone%3A${title}\nDoes everything look right?` + ) + + if (!looksRight) { + const undoPRs = await confirm( + `${ASK} Do you want to undo the changes to the PRs?` + ) + + if (undoPRs) { + await Promise.all( + pullRequestIds.map((pullRequestId) => + updatePullRequestMilestone(pullRequestId, nextReleaseMilestoneId) + ) + ) + } + + const undoMilestone = await confirm( + `${ASK} Do you want to delete the milestone` + ) + + if (undoMilestone) { + await deleteMilestone(milestone.number) + } + + return + } + + return milestone +} + +// Helpers + +/** + * @typedef {{ + * repository: { + * milestones: { + * nodes: Array<{ title: string, id: string, number: number }> + * } + * } + * }} GetMilestonesRes + * + * @param {string} [title] + */ +async function getMilestone(title) { + const { + repository: { + milestones: { nodes: milestones }, + }, + } = /** @type GetMilestonesRes */ ( + await octokit.graphql(GET_MILESTONES, { title }) + ) + + let milestone = milestones.find((milestone) => milestone.title === title) + + return milestone +} + +export const GET_MILESTONES = ` + query GetMilestoneIds($title: String) { + repository(owner: "redwoodjs", name: "redwood") { + milestones( + query: $title + first: 3 + orderBy: { field: NUMBER, direction: DESC } + ) { + nodes { + title + id + number + } + } + } + } +` + +/** + * @param {string} title + * @returns {Promise<{ data: { node_id: string, number: number } }>} + */ +function createMilestone(title) { + // GitHub doesn't have a GraphQL API for creating milestones, so REST it is. + return octokit.request('POST /repos/{owner}/{repo}/milestones', { + owner: 'redwoodjs', + repo: 'redwood', + title, + }) +} + +/** + * @typedef {{ + * repository: { + * milestones: { + * nodes: Array<{ title: string, id: string }> + * } + * } + * }} GetNextReleaseMilestoneIdRes + */ +async function getNextReleaseMilestoneId() { + const { + repository: { + milestones: { nodes: milestones }, + }, + } = /** @type {GetNextReleaseMilestoneIdRes} */ ( + await octokit.graphql(GET_NEXT_RELEASE_MILESTONE_ID) + ) + + const { id } = milestones.find( + (milestone) => milestone.title === 'next-release' + ) + + return id +} + +export const GET_NEXT_RELEASE_MILESTONE_ID = ` + query GetNextReleaseMilestoneId { + repository(owner: "redwoodjs", name: "redwood") { + milestones(query: "next-release", first: 5) { + nodes { + title + id + } + } + } + } +` + +/** + * @typedef {{ + * node: { + * pullRequests: { + * nodes: Array<{ id: string }> + * } + * } + * }} GetNextReleasePullRequestIdsRes + * + * @param {string} milestoneId + */ +export async function getPullRequestIdsWithMilestone(milestoneId) { + /** + * Right now we're not handling the case that we merge more than 100 PRs. + */ + const { + node: { + pullRequests: { nodes: pullRequests }, + }, + } = /** @type {GetNextReleasePullRequestIdsRes} */ ( + await octokit.graphql(GET_NEXT_RELEASE_PULL_REQUEST_IDS, { + milestoneId, + }) + ) + + return pullRequests.map((pullRequest) => pullRequest.id) +} + +export const GET_NEXT_RELEASE_PULL_REQUEST_IDS = ` + query GetNextReleasePullRequestIds($milestoneId: ID!) { + node(id: $milestoneId) { + ... on Milestone { + pullRequests(first: 100) { + nodes { + id + } + } + } + } + } +` + +/** + * @param {string} pullRequestId + * @param {string} milestoneId + */ +function updatePullRequestMilestone(pullRequestId, milestoneId) { + return octokit.graphql(UPDATE_NEXT_RELEASE_PULL_REQUEST_MILESTONE, { + pullRequestId, + milestoneId, + }) +} + +export const UPDATE_NEXT_RELEASE_PULL_REQUEST_MILESTONE = ` + mutation UpdatePullRequestMilestone($pullRequestId: ID!, $milestoneId: ID!) { + updatePullRequest( + input: { pullRequestId: $pullRequestId, milestoneId: $milestoneId } + ) { + clientMutationId + } + } +` + +/** + * @param {number} milestone_number + */ +export function closeMilestone(milestone_number) { + return octokit.request( + 'POST /repos/{owner}/{repo}/milestones/{milestone_number}', + { + owner: 'redwoodjs', + repo: 'redwood', + milestone_number, + state: 'closed', + } + ) +} + +/** + * @param {number} milestone_number + */ +function deleteMilestone(milestone_number) { + return octokit.request( + 'DELETE /repos/{owner}/{repo}/milestones/{milestone_number}', + { + owner: 'redwoodjs', + repo: 'redwood', + milestone_number, + } + ) +} diff --git a/tasks/release/updateNextReleasePullRequestsMilestone.test.mjs b/tasks/release/updateNextReleasePullRequestsMilestone.test.mjs new file mode 100644 index 000000000000..b80e1621c612 --- /dev/null +++ b/tasks/release/updateNextReleasePullRequestsMilestone.test.mjs @@ -0,0 +1,237 @@ +/* eslint-env jest, es2021 */ +import { rest, graphql } from 'msw' +import { setupServer } from 'msw/node' + +import updateNextReleasePullRequestsMilestone, { + GET_MILESTONES, + GET_NEXT_RELEASE_MILESTONE_ID, + GET_NEXT_RELEASE_PULL_REQUEST_IDS, + UPDATE_NEXT_RELEASE_PULL_REQUEST_MILESTONE, +} from './updateNextReleasePullRequestsMilestone.mjs' + +/** + * MSW setup + */ +let nextVersionMilestone + +const handleCreateMilestone = rest.post( + 'https://api.github.com/repos/:owner/:repo/milestones', + (req, res, ctx) => { + const { title } = req.body + + if (!title) { + return res(ctx.status(422)) + } + + const { owner, repo } = req.params + + if (owner !== 'redwoodjs' || repo !== 'redwood') { + return res(ctx.status(404)) + } + + nextVersionMilestone = { + node_id: `123-${title}`, + number: 1, + } + + return res(ctx.json(nextVersionMilestone)) + } +) + +const nextReleaseMilestone = { + title: 'next-release', + id: 'MI_kwDOC2M2f84Aa82f', +} + +const handleGetMilestoneIds = graphql.query( + 'GetMilestoneIds', + (_req, res, ctx) => { + return res( + ctx.data({ + repository: { + milestones: { + nodes: [ + { + title: 'next-release-patch', + id: 'MDk6TWlsZXN0b25lNjc1Nzk0MQ==', + }, + { + title: 'next-release-priority', + id: 'MDk6TWlsZXN0b25lNjc3MTI3MQ==', + }, + nextReleaseMilestone, + ], + }, + }, + }) + ) + } +) + +const pullRequests = [ + { + id: 'PR_kwDOC2M2f84svWtA', + }, + { + id: 'PR_kwDOC2M2f84xGIzx', + }, + { + id: 'PR_kwDOC2M2f84xHG6Y', + }, +] + +const handleGetNextReleasePullRequestIds = graphql.query( + 'GetNextReleasePullRequestIds', + (req, res, ctx) => { + const { milestoneId } = req.variables + + if (milestoneId !== nextReleaseMilestone.id) { + return res( + ctx.data({ + node: null, + }) + ) + } + + return res( + ctx.data({ + node: { + pullRequests: { + nodes: pullRequests, + }, + }, + }) + ) + } +) + +const handleUpdatePullRequestMilestone = graphql.mutation( + 'UpdatePullRequestMilestone', + (req, res, ctx) => { + const { pullRequestId, milestoneId } = req.variables + + if ( + milestoneId !== nextVersionMilestone.node_id || + !pullRequests.map((pullRequest) => pullRequest.id).includes(pullRequestId) + ) { + return res( + ctx.data({ + updatePullRequest: null, + }) + ) + } + + return res( + ctx.data({ + updatePullRequest: { + clientMutationId: '123', + }, + }) + ) + } +) + +const handleCloseMilestone = rest.post( + 'https://api.github.com/repos/:owner/:repo/milestones/:milestone_number', + (req, res, ctx) => { + const { milestone_number } = req.params + + if (+milestone_number !== nextVersionMilestone.number) { + return res(ctx.status(404)) + } + + const { state } = req.body + + return res( + ctx.json({ + state, + }) + ) + } +) + +const server = setupServer( + handleCreateMilestone, + handleGetMilestoneIds, + handleGetNextReleasePullRequestIds, + handleUpdatePullRequestMilestone, + handleCloseMilestone +) + +beforeAll(() => server.listen()) + +afterAll(() => server.close()) + +describe('updateNextReleasePullRequestsMilestone', () => { + it('uses the right queries', () => { + expect(GET_MILESTONES).toMatchInlineSnapshot(` + " + query GetMilestoneIds($title: String) { + repository(owner: \\"redwoodjs\\", name: \\"redwood\\") { + milestones( + query: $title + first: 3 + orderBy: { field: NUMBER, direction: DESC } + ) { + nodes { + title + id + number + } + } + } + } + " + `) + + expect(GET_NEXT_RELEASE_MILESTONE_ID).toMatchInlineSnapshot(` + " + query GetNextReleaseMilestoneId { + repository(owner: \\"redwoodjs\\", name: \\"redwood\\") { + milestones(query: \\"next-release\\", first: 5) { + nodes { + title + id + } + } + } + } + " + `) + + expect(GET_NEXT_RELEASE_PULL_REQUEST_IDS).toMatchInlineSnapshot(` + " + query GetNextReleasePullRequestIds($milestoneId: ID!) { + node(id: $milestoneId) { + ... on Milestone { + pullRequests(first: 100) { + nodes { + id + } + } + } + } + } + " + `) + + expect(UPDATE_NEXT_RELEASE_PULL_REQUEST_MILESTONE).toMatchInlineSnapshot(` + " + mutation UpdatePullRequestMilestone($pullRequestId: ID!, $milestoneId: ID!) { + updatePullRequest( + input: { pullRequestId: $pullRequestId, milestoneId: $milestoneId } + ) { + clientMutationId + } + } + " + `) + }) + + /** + * This needs a few tweaks. + */ + it.skip('MSW unit test', async () => { + await updateNextReleasePullRequestsMilestone('v0.42.1') + }) +}) diff --git a/yarn.lock b/yarn.lock index ac8f5363a9ee..b7783868d630 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7335,7 +7335,7 @@ __metadata: languageName: node linkType: hard -"@types/fs-extra@npm:9.0.13": +"@types/fs-extra@npm:9.0.13, @types/fs-extra@npm:^9.0.12": version: 9.0.13 resolution: "@types/fs-extra@npm:9.0.13" dependencies: @@ -7662,7 +7662,7 @@ __metadata: languageName: node linkType: hard -"@types/minimist@npm:^1.2.0": +"@types/minimist@npm:^1.2.0, @types/minimist@npm:^1.2.2": version: 1.2.2 resolution: "@types/minimist@npm:1.2.2" checksum: f220f57f682bbc3793dab4518f8e2180faa79d8e2589c79614fd777d7182be203ba399020c3a056a115064f5d57a065004a32b522b2737246407621681b24137 @@ -7683,7 +7683,7 @@ __metadata: languageName: node linkType: hard -"@types/node-fetch@npm:2.5.12, @types/node-fetch@npm:^2.5.7": +"@types/node-fetch@npm:2.5.12, @types/node-fetch@npm:^2.5.12, @types/node-fetch@npm:^2.5.7": version: 2.5.12 resolution: "@types/node-fetch@npm:2.5.12" dependencies: @@ -7693,7 +7693,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:16.11.21, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0": +"@types/node@npm:*, @types/node@npm:16.11.21, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0, @types/node@npm:^16.6": version: 16.11.21 resolution: "@types/node@npm:16.11.21" checksum: e5af56bf8fdfd24a15a427f407f9b0a225fec2e8b91e1fdc365fdd2ab8ca19b9cb4c9e0f37494f97c5dce9983c9589b8e746ecc9592066523ca05949dd9b1fbd @@ -7779,6 +7779,13 @@ __metadata: languageName: node linkType: hard +"@types/prompts@npm:2.4.0": + version: 2.4.0 + resolution: "@types/prompts@npm:2.4.0" + checksum: 24bb6d7021c5f50ac1265a2d0634fff1ccd2b4c40b6a8b55728ce5d9c17d132ea8f57b848d6601002d235d5d29cf100ae902ed60ef4a06091cf4b640b9e411cb + languageName: node + linkType: hard + "@types/prop-types@npm:*": version: 15.7.4 resolution: "@types/prop-types@npm:15.7.4" @@ -13474,7 +13481,7 @@ __metadata: languageName: node linkType: hard -"duplexer@npm:^0.1.1, duplexer@npm:^0.1.2": +"duplexer@npm:^0.1.1, duplexer@npm:^0.1.2, duplexer@npm:~0.1.1": version: 0.1.2 resolution: "duplexer@npm:0.1.2" checksum: c57bcd4bdf7e623abab2df43a7b5b23d18152154529d166c1e0da6bee341d84c432d157d7e97b32fecb1bf3a8b8857dd85ed81a915789f550637ed25b8e64fc2 @@ -14559,6 +14566,21 @@ __metadata: languageName: node linkType: hard +"event-stream@npm:=3.3.4": + version: 3.3.4 + resolution: "event-stream@npm:3.3.4" + dependencies: + duplexer: ~0.1.1 + from: ~0 + map-stream: ~0.1.0 + pause-stream: 0.0.11 + split: 0.3 + stream-combiner: ~0.0.4 + through: ~2.3.1 + checksum: c3ec4e1efc27ab3e73a98923f0a2fa9a19051b87068fea2f3d53d2e4e8c5cfdadf8c8a115b17f3d90b16a46432d396bad91b6e8d0cceb3e449be717a03b75209 + languageName: node + linkType: hard + "event-target-shim@npm:^5.0.0": version: 5.0.1 resolution: "event-target-shim@npm:5.0.1" @@ -15670,6 +15692,13 @@ __metadata: languageName: node linkType: hard +"from@npm:~0": + version: 0.1.7 + resolution: "from@npm:0.1.7" + checksum: 3aab5aea8fe8e1f12a5dee7f390d46a93431ce691b6222dcd5701c5d34378e51ca59b44967da1105a0f90fcdf5d7629d963d51e7ccd79827d19693bdcfb688d4 + languageName: node + linkType: hard + "fromentries@npm:^1.3.1": version: 1.3.2 resolution: "fromentries@npm:1.3.2" @@ -15684,7 +15713,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:10.0.0": +"fs-extra@npm:10.0.0, fs-extra@npm:^10.0.0": version: 10.0.0 resolution: "fs-extra@npm:10.0.0" dependencies: @@ -16284,17 +16313,17 @@ fsevents@^1.2.7: languageName: node linkType: hard -"globby@npm:^12.0.2": - version: 12.0.2 - resolution: "globby@npm:12.0.2" +"globby@npm:^12.0.1, globby@npm:^12.0.2": + version: 12.2.0 + resolution: "globby@npm:12.2.0" dependencies: array-union: ^3.0.1 dir-glob: ^3.0.1 fast-glob: ^3.2.7 - ignore: ^5.1.8 + ignore: ^5.1.9 merge2: ^1.4.1 slash: ^4.0.0 - checksum: 61057effcc7edc7ca7ba9e27a435f08e05b2b8d80c13d2a18e14804936efcc7b1fae528536b6e6f85969f0a76fb0c86d65b3003f39e060457a2e524a3016b704 + checksum: 121fee62bb9a43a35a32731cda9540241003ef578f9cee5ad87b27d3020b94857ff62f8d82cb99dbeedf6f26981c9fa62509d873392642ceb37674f3d6ec4e52 languageName: node linkType: hard @@ -17370,7 +17399,7 @@ fsevents@^1.2.7: languageName: node linkType: hard -"ignore@npm:^5.1.8, ignore@npm:^5.2.0": +"ignore@npm:^5.1.8, ignore@npm:^5.1.9, ignore@npm:^5.2.0": version: 5.2.0 resolution: "ignore@npm:5.2.0" checksum: 7fb7b4c4c52c2555113ff968f8a83b8ac21b076282bfcb3f468c3fb429be69bd56222306c31de95dd452c647fc6ae24339b8047ebe3ef34c02591abfec58da01 @@ -20513,6 +20542,13 @@ fsevents@^1.2.7: languageName: node linkType: hard +"map-stream@npm:~0.1.0": + version: 0.1.0 + resolution: "map-stream@npm:0.1.0" + checksum: 7dd6debe511c1b55d9da75e1efa65a28b1252a2d8357938d2e49b412713c478efbaefb0cdf0ee0533540c3bf733e8f9f71e1a15aa0fe74bf71b64e75bf1576bd + languageName: node + linkType: hard + "map-values@npm:^1.0.1": version: 1.0.1 resolution: "map-values@npm:1.0.1" @@ -22846,6 +22882,15 @@ fsevents@^1.2.7: languageName: node linkType: hard +"pause-stream@npm:0.0.11": + version: 0.0.11 + resolution: "pause-stream@npm:0.0.11" + dependencies: + through: ~2.3 + checksum: 86f12c64cdaaa8e45ebaca4e39a478e1442db8b4beabc280b545bfaf79c0e2f33c51efb554aace5c069cc441c7b924ba484837b345eaa4ba6fc940d62f826802 + languageName: node + linkType: hard + "pbkdf2@npm:^3.0.17, pbkdf2@npm:^3.0.3": version: 3.1.2 resolution: "pbkdf2@npm:3.1.2" @@ -23902,6 +23947,17 @@ fsevents@^1.2.7: languageName: node linkType: hard +"ps-tree@npm:^1.2.0": + version: 1.2.0 + resolution: "ps-tree@npm:1.2.0" + dependencies: + event-stream: =3.3.4 + bin: + ps-tree: ./bin/ps-tree.js + checksum: 9d1c159e0890db5aa05f84d125193c2190a6c4ecd457596fd25e7611f8f747292a846459dcc0244e27d45529d4cea6d1010c3a2a087fad02624d12fdb7d97c22 + languageName: node + linkType: hard + "pseudomap@npm:^1.0.1": version: 1.0.2 resolution: "pseudomap@npm:1.0.2" @@ -25428,11 +25484,13 @@ resolve@^2.0.0-next.3: "@types/jest": 27.4.0 "@types/jscodeshift": 0.11.2 "@types/lodash.template": 4.5.0 + "@types/prompts": 2.4.0 all-contributors-cli: 6.20.0 ansi-colors: 4.1.1 babel-jest: 27.4.6 babel-plugin-auto-import: 1.1.0 babel-plugin-remove-code: 0.0.6 + boxen: 5.1.2 core-js: 3.20.3 cypress: 9.3.1 cypress-wait-until: 1.7.2 @@ -25442,14 +25500,17 @@ resolve@^2.0.0-next.3: jscodeshift: 0.13.0 lerna: 4.0.0 lodash.template: 4.5.0 + msw: 0.36.7 nodemon: 2.0.15 npm-packlist: 3.0.0 octokit: 1.7.1 ora: 5.4.1 + prompts: 2.4.2 rimraf: 3.0.2 terminal-link: 2.1.1 typescript: 4.5.5 typescript-transform-paths: 3.3.1 + zx: 4.3.0 languageName: unknown linkType: soft @@ -26503,6 +26564,15 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard +"split@npm:0.3": + version: 0.3.3 + resolution: "split@npm:0.3.3" + dependencies: + through: 2 + checksum: 88c09b1b4de84953bf5d6c153123a1fbb20addfea9381f70d27b4eb6b2bfbadf25d313f8f5d3fd727d5679b97bfe54da04766b91010f131635bf49e51d5db3fc + languageName: node + linkType: hard + "split@npm:^1.0.0": version: 1.0.1 resolution: "split@npm:1.0.1" @@ -26668,6 +26738,15 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard +"stream-combiner@npm:~0.0.4": + version: 0.0.4 + resolution: "stream-combiner@npm:0.0.4" + dependencies: + duplexer: ~0.1.1 + checksum: 8075a94c0eb0f20450a8236cb99d4ce3ea6e6a4b36d8baa7440b1a08cde6ffd227debadffaecd80993bd334282875d0e927ab5b88484625e01970dd251004ff5 + languageName: node + linkType: hard + "stream-each@npm:^1.1.0": version: 1.2.3 resolution: "stream-each@npm:1.2.3" @@ -27594,7 +27673,7 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard -"through@npm:2, through@npm:>=2.2.7 <3, through@npm:^2.3.4, through@npm:^2.3.6, through@npm:^2.3.8": +"through@npm:2, through@npm:>=2.2.7 <3, through@npm:^2.3.4, through@npm:^2.3.6, through@npm:^2.3.8, through@npm:~2.3, through@npm:~2.3.1": version: 2.3.8 resolution: "through@npm:2.3.8" checksum: 4b09f3774099de0d4df26d95c5821a62faee32c7e96fb1f4ebd54a2d7c11c57fe88b0a0d49cf375de5fee5ae6bf4eb56dbbf29d07366864e2ee805349970d3cc @@ -30234,3 +30313,24 @@ resolve@^2.0.0-next.3: checksum: 26dc7d32e5596824b565db1da9650d00d32659c1211195bef50c25c60820f9c942aa7abefe678fc1ed0b97c1755036ac1bde5f97881d7d0e73e04e02aca56957 languageName: node linkType: hard + +"zx@npm:4.3.0": + version: 4.3.0 + resolution: "zx@npm:4.3.0" + dependencies: + "@types/fs-extra": ^9.0.12 + "@types/minimist": ^1.2.2 + "@types/node": ^16.6 + "@types/node-fetch": ^2.5.12 + chalk: ^4.1.2 + fs-extra: ^10.0.0 + globby: ^12.0.1 + minimist: ^1.2.5 + node-fetch: ^2.6.1 + ps-tree: ^1.2.0 + which: ^2.0.2 + bin: + zx: zx.mjs + checksum: 04f83a76bf0f75c08d36b9590f14e4aeddc6d70533af3cb90af34a26f64e07f3e9b65f95f3fba5fa2e8475cd302514b436c5b77d3af99185593a9e730fe540ec + languageName: node + linkType: hard