diff --git a/README.md b/README.md index 03fb451..4f5274b 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,14 @@ ## Install the bot at https://github.com/apps/changeset-bot - - This bot will comment on PRs saying that either a user might need to add a changeset(note that PRs changing things like documentation generally don't need a changeset)or say that the PR is good and already has a changeset. +It wil also comment on released Pull Requests/Issues. + screenshot of changeset bot message from https://github.com/mitchellhamilton/manypkg/pull/18 before a changeset was added screenshot of the changeset bot message from https://github.com/mitchellhamilton/manypkg/pull/18 showing the changeset good to go message - Sometimes, a contributor won't add a changeset to a PR but you might want to merge in the PR without having to wait on them to add it. To address this, this bot adds a link with the filename pre-filled to add a changeset so all you have to do is write the changeset and click commit. screenshot of the changeset bot message from https://github.com/mitchellhamilton/manypkg/pull/18 focused on the create a changeset link @@ -27,17 +26,17 @@ When writing the changeset, it should look something like this with the packages ```markdown --- -'@changesets/cli': major -'@changesets/read': minor +"@changesets/cli": major +"@changesets/read": minor --- A very helpful description of the changes ``` + --- The information below is for contributing to the bot. - ## Setup ```sh diff --git a/app.yml b/app.yml index e4b2b54..67ba1a9 100644 --- a/app.yml +++ b/app.yml @@ -82,7 +82,7 @@ default_permissions: # Pull requests and related comments, assignees, labels, milestones, and merges. # https://developer.github.com/v3/apps/permissions/#permission-on-pull-requests - pull_requests: write + pull_requests: write # Manage the post-receive hooks for a repository. # https://developer.github.com/v3/apps/permissions/#permission-on-repository-hooks diff --git a/index.ts b/index.ts index 46a367f..86bfbb4 100644 --- a/index.ts +++ b/index.ts @@ -11,6 +11,7 @@ import { import markdownTable from "markdown-table"; import { captureException } from "@sentry/node"; import { ValidationError } from "@changesets/errors"; +import issueParser from "issue-parser"; const getReleasePlanMessage = (releasePlan: ReleasePlan | null) => { if (!releasePlan) return ""; @@ -91,6 +92,14 @@ Not sure what this means? [Click here to learn what changesets are](https://git `; +const getReleaseMessage = ( + html_url: string, + name: string +) => `### 🦋 This work has been released in release version: ${name} + +Release link: ${html_url} +`; + const getNewChangesetTemplate = (changedPackages: string[], title: string) => encodeURIComponent(`--- ${changedPackages.map((x) => `"${x}": patch`).join("\n")} @@ -100,6 +109,21 @@ ${title} `); type PRContext = Context; +type ReleaseContext = Context; + +const getSearchQueries = (base: string, commits: string[]) => { + return commits.reduce((searches, commit) => { + const lastSearch = searches[searches.length - 1]; + + if (lastSearch && lastSearch.length + commit.length <= 256 - 1) { + searches[searches.length - 1] = `${lastSearch}+hash:${commit}`; + } else { + searches.push(`${base}+hash:${commit}`); + } + + return searches; + }, [] as string[]); +}; const getCommentId = ( context: PRContext, @@ -130,6 +154,139 @@ export default (app: Application) => { app.auth(); app.log("Yay, the app was loaded!"); + /* Comment on released Pull Requests/Issues */ + app.on("release.published", async (context: ReleaseContext) => { + /* + Here are the following steps to retrieve the released PRs and issues. + + 1. Retrieve the tag associated with the release + 2. Take the commit sha associated with the tag + 3. Retrieve all the commits starting from the tag commit sha + 4. Retrieve the PRs with commits sha matching the release commits + 5. Map through the list of commits and the list of PRs to + find commit message or PRs body that closes an issue and + get the issue number. + 6. Create a comment for each issue and PR + */ + + const release = context.payload.release; + const { html_url, tag_name } = release; + const repo = { + repo: context.payload.repository.name, + owner: context.payload.repository.owner.login, + }; + + let tagPage = 0; + let tagFound = false; + let tagCommitSha = ""; + + /* 1 */ + while (!tagFound) { + await context.github.repos + .listTags({ + ...repo, + per_page: 100, + page: tagPage, + }) + .then(({ data }) => { + const tag = data.find((el) => el.name === tag_name); + if (tag) { + tagFound = true; + /* 2 */ + tagCommitSha = tag.commit.sha; + } + tagPage += 1; + }) + .catch((err) => console.warn(err)); + } + + /* 3 */ + const commits = await context.github.repos + .listCommits({ + ...repo, + sha: tagCommitSha, + }) + .then(({ data }) => data); + + const shas = commits.map(({ sha }) => sha); + + /* Build a seach query to retrieve pulls with commit hashes. + * example: repo:/+type:pr+is:merged+hash:+hash:... + */ + const searchQueries = getSearchQueries( + `repo:${repo.owner}/${repo.repo}+type:pr+is:merged`, + shas + ).map( + async (q) => + (await context.github.search.issuesAndPullRequests({ q })).data.items + ); + + const queries = await (await Promise.all(searchQueries)).flat(); + + const queriesSet = queries.map((el) => el.number); + + const filteredQueries = queries.filter( + (el, i) => queriesSet.indexOf(el.number) === i + ); + + /* 4 */ + const pulls = await filteredQueries.filter( + async ({ number }) => + ( + await context.github.pulls.listCommits({ + owner: repo.owner, + repo: repo.repo, + pull_number: number, + }) + ).data.find(({ sha }) => shas.includes(sha)) || + shas.includes( + ( + await context.github.pulls.get({ + owner: repo.owner, + repo: repo.repo, + pull_number: number, + }) + ).data.merge_commit_sha + ) + ); + + const parser = issueParser("github"); + + /* 5 */ + const issues = [ + ...pulls.map((pr) => pr.body), + ...commits.map(({ commit }) => commit.message), + ].reduce((issues, message) => { + return message + ? issues.concat( + parser(message) + .actions.close.filter( + (action) => + action.slug === null || + action.slug === undefined || + action.slug === `${repo.owner}/${repo.repo}` + ) + .map((action) => ({ number: Number.parseInt(action.issue, 10) })) + ) + : issues; + }, [] as { number: number }[]); + + /* 6 */ + await Promise.all( + [...new Set([...pulls, ...issues].map(({ number }) => number))].map( + async (number) => { + const issueComment = { + ...repo, + issue_number: number, + body: getReleaseMessage(html_url, tag_name), + }; + + context.github.issues.createComment(issueComment); + } + ) + ); + }); + app.on( ["pull_request.opened", "pull_request.synchronize"], async (context: PRContext) => { diff --git a/package.json b/package.json index 4874742..f645efb 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@types/micromatch": "^4.0.1", "@types/node-fetch": "^2.5.5", "human-id": "^1.0.2", + "issue-parser": "^6.0.0", "js-yaml": "^3.14.0", "markdown-table": "^2.0.0", "node-fetch": "^2.6.1", @@ -27,6 +28,7 @@ "typescript": "^4.0.3" }, "devDependencies": { + "@types/issue-parser": "^3.0.0", "jest": "^24.1.0", "nock": "^10.0.0", "outdent": "^0.7.0" diff --git a/yarn.lock b/yarn.lock index ee67a0c..8af3dc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1046,6 +1046,11 @@ dependencies: "@types/node" "*" +"@types/issue-parser@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/issue-parser/-/issue-parser-3.0.0.tgz#154dcdea73c3447b0e30d8ab3dfe2661a531884b" + integrity sha512-wDi9vfrRlosge6GIjC8ToxeiyG7qYSNWRwZIxav0uPIn7LfGdo6RsKztAAFReXipFM5vDIYGgBiAEXcZ/EIniw== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" @@ -3291,6 +3296,17 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= +issue-parser@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/issue-parser/-/issue-parser-6.0.0.tgz#b1edd06315d4f2044a9755daf85fdafde9b4014a" + integrity sha512-zKa/Dxq2lGsBIXQ7CUZWTHfvxPC2ej0KfO7fIPqLlHB9J2hJ7rGhZ5rilhuufylr4RXYPzJUeFjKxz305OsNlA== + dependencies: + lodash.capitalize "^4.2.1" + lodash.escaperegexp "^4.1.2" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.uniqby "^4.7.0" + istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" @@ -3954,11 +3970,21 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash.capitalize@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9" + integrity sha1-+CbJtOKoUR2E46yinbBeGk87cqk= + lodash.defaults@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= +lodash.escaperegexp@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" + integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c= + lodash.flatten@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" @@ -4004,12 +4030,12 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= -lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.5: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lodash.uniqby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" + integrity sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI= -lodash@^4.17.19: +lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.19, lodash@^4.17.5: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==