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.
+
-
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.
@@ -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==