Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<img width="1552" alt="screenshot of changeset bot message from https://github.com/mitchellhamilton/manypkg/pull/18 before a changeset was added" src="https://user-images.githubusercontent.com/11481355/66183943-dc418680-e6bd-11e9-998d-e43f90a974bd.png">

<img width="1552" alt="screenshot of the changeset bot message from https://github.com/mitchellhamilton/manypkg/pull/18 showing the changeset good to go message" src="https://user-images.githubusercontent.com/11481355/66184229-cf716280-e6be-11e9-950e-0f64a31dbf15.png">


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.

<img width="1552" alt="screenshot of the changeset bot message from https://github.com/mitchellhamilton/manypkg/pull/18 focused on the create a changeset link" src="https://user-images.githubusercontent.com/11481355/66184052-3a6e6980-e6be-11e9-8e62-8fd9d49af587.png">
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
157 changes: 157 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 "";
Expand Down Expand Up @@ -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")}
Expand All @@ -100,6 +109,21 @@ ${title}
`);

type PRContext = Context<Webhooks.EventPayloads.WebhookPayloadPullRequest>;
type ReleaseContext = Context<Webhooks.EventPayloads.WebhookPayloadRelease>;

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,
Expand Down Expand Up @@ -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:<OWNER>/<REPO>+type:pr+is:merged+hash:<FIRST_COMMIT_HASH>+hash:<SECOND_COMMIT_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) => {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand Down
36 changes: 31 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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==
Expand Down