Skip to content
Merged
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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,10 @@ Once [installed](#usage), this integration will create a
[check](https://developer.github.com/v3/checks/runs/) indicating whether
commits in a Pull Request do not contain a valid `Signed-off-by` line.

Comment `@dcoapp recheck` on an open Pull Request to re-run or create the DCO
check, which can recover PRs stuck waiting for status after an outage.
To re-run or create the DCO check on an open Pull Request, submit a Pull
Request review with `@dcoapp recheck` on its own line in the review summary.
You can also add an inline review comment with `@dcoapp recheck` on its own
line. This can recover PRs stuck waiting for status after an outage.

![DCO success](https://user-images.githubusercontent.com/13410355/42352738-35f4e690-8071-11e8-9c8c-260e5868bfc8.png)
![DCO failure](https://user-images.githubusercontent.com/13410355/42352794-85fe1c9c-8071-11e8-834a-05a4aeb8cc90.png)
Expand Down
3 changes: 2 additions & 1 deletion REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ path = [
"test/__snapshots__/index.test.js.snap",
"test/fixtures/compare-success.json",
"test/fixtures/compare.json",
"test/fixtures/issue_comment.created.json",
"test/fixtures/pull_request.opened-success.json",
"test/fixtures/pull_request.opened.json",
"test/fixtures/pull_request_review.submitted.json",
"test/fixtures/pull_request_review_comment.created.json",
"test/fixtures/push.not-signed-off.json",
"test/fixtures/push.signed-off.json",
]
Expand Down
6 changes: 3 additions & 3 deletions app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ default_events:
# - deployment_status
# - fork
# - gollum
- issue_comment
# - issue_comment
# - issues
# - label
# - milestone
Expand All @@ -40,8 +40,8 @@ default_events:
# - project_column
# - public
- pull_request
# - pull_request_review
# - pull_request_review_comment
- pull_request_review
- pull_request_review_comment
- push
# - release
# - repository
Expand Down
31 changes: 21 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,17 +143,28 @@ module.exports = (app) => {
}
}

app.on("issue_comment.created", onRecheckComment);
async function onRecheckComment(context) {
const { comment, issue } = context.payload;
if (!issue.pull_request) return;
if (comment.user.type === "Bot") return;
if (issue.state !== "open") return;
if (comment.body.trim().toLowerCase() !== "@dcoapp recheck") return;
function isRecheckCommand(body) {
if (!body) return false;
return body
.split(/\r?\n/)
.some((line) => line.trim().toLowerCase() === "@dcoapp recheck");
}

const { data: pr } = await context.octokit.rest.pulls.get(
context.repo({ pull_number: issue.number })
);
app.on("pull_request_review.submitted", onRecheckReview);
async function onRecheckReview(context) {
const { review, pull_request: pr } = context.payload;
if (review.user.type === "Bot") return;
if (pr.state !== "open") return;
if (!isRecheckCommand(review.body)) return;
await check(context, pr);
}

app.on("pull_request_review_comment.created", onRecheckReviewComment);
async function onRecheckReviewComment(context) {
const { comment, pull_request: pr } = context.payload;
if (comment.user.type === "Bot") return;
if (pr.state !== "open") return;
if (!isRecheckCommand(comment.body)) return;
await check(context, pr);
}

Expand Down
34 changes: 0 additions & 34 deletions test/fixtures/issue_comment.created.json

This file was deleted.

459 changes: 459 additions & 0 deletions test/fixtures/pull_request_review.submitted.json

Large diffs are not rendered by default.

455 changes: 455 additions & 0 deletions test/fixtures/pull_request_review_comment.created.json

Large diffs are not rendered by default.

165 changes: 136 additions & 29 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const dco = require("..");

const payload = require("./fixtures/pull_request.opened");
const payloadSuccess = require("./fixtures/pull_request.opened-success");
const issueCommentPayload = require("./fixtures/issue_comment.created");
const pullRequestReviewPayload = require("./fixtures/pull_request_review.submitted");
const pullRequestReviewCommentPayload = require("./fixtures/pull_request_review_comment.created");
const compare = require("./fixtures/compare");
const compareSuccess = require("./fixtures/compare-success");

Expand Down Expand Up @@ -545,47 +546,40 @@ allowRemediationCommits:
});
});

describe("issue_comment.created event", () => {
test("ignores comments that are not on a pull request", async () => {
const payload = structuredClone(issueCommentPayload);
delete payload.issue.pull_request;
describe("pull_request_review.submitted event", () => {
test("ignores reviews from bots", async () => {
const payload = structuredClone(pullRequestReviewPayload);
payload.review.user.type = "Bot";

await probot.receive({ name: "issue_comment", payload });
await probot.receive({ name: "pull_request_review", payload });
});

test("ignores comments from bots", async () => {
const payload = structuredClone(issueCommentPayload);
payload.comment.user.type = "Bot";
test("ignores reviews on closed pull requests", async () => {
const payload = structuredClone(pullRequestReviewPayload);
payload.pull_request.state = "closed";

await probot.receive({ name: "issue_comment", payload });
await probot.receive({ name: "pull_request_review", payload });
});

test("ignores comments on closed pull requests", async () => {
const payload = structuredClone(issueCommentPayload);
payload.issue.state = "closed";
test("ignores reviews with empty bodies", async () => {
const payload = structuredClone(pullRequestReviewPayload);
payload.review.body = null;

await probot.receive({ name: "issue_comment", payload });
await probot.receive({ name: "pull_request_review", payload });
});

test("ignores comments without the exact recheck command", async () => {
const payload = structuredClone(issueCommentPayload);
payload.comment.body = "@dcoapp recheck please";
test("ignores reviews without the recheck command on its own line", async () => {
const payload = structuredClone(pullRequestReviewPayload);
payload.review.body = "please @dcoapp recheck";

await probot.receive({ name: "issue_comment", payload });
await probot.receive({ name: "pull_request_review", payload });
});

test("creates a passing check for recheck comments", async () => {
const payload = structuredClone(issueCommentPayload);
payload.comment.body = " @DCOApp Recheck ";
const pullRequest = {
...payloadSuccess.pull_request,
closed_at: null,
state: "open",
};
test("creates a passing check for recheck reviews", async () => {
const payload = structuredClone(pullRequestReviewPayload);
payload.review.body = "please run\n @DCOApp Recheck \nthanks";

const mock = nock("https://api.github.com")
.get("/repos/octocat/Hello-World/pulls/1")
.reply(200, pullRequest)
// no config
.get("/repos/octocat/Hello-World/contents/.github%2Fdco.yml")
.reply(404)
Expand Down Expand Up @@ -616,7 +610,120 @@ allowRemediationCommits:
})
.reply(200);

await probot.receive({ name: "issue_comment", payload });
await probot.receive({ name: "pull_request_review", payload });

expect(mock.activeMocks()).toStrictEqual([]);
});
});

describe("pull_request_review_comment.created event", () => {
test("ignores review comments from bots", async () => {
const payload = structuredClone(pullRequestReviewCommentPayload);
payload.comment.user.type = "Bot";

await probot.receive({ name: "pull_request_review_comment", payload });
});

test("ignores review comments on closed pull requests", async () => {
const payload = structuredClone(pullRequestReviewCommentPayload);
payload.pull_request.state = "closed";

await probot.receive({ name: "pull_request_review_comment", payload });
});

test("ignores review comments with empty bodies", async () => {
const payload = structuredClone(pullRequestReviewCommentPayload);
payload.comment.body = null;

await probot.receive({ name: "pull_request_review_comment", payload });
});

test("ignores review comments without the recheck command on its own line", async () => {
const payload = structuredClone(pullRequestReviewCommentPayload);
payload.comment.body = "@dcoapp recheck please";

await probot.receive({ name: "pull_request_review_comment", payload });
});

test("creates a failing check for recheck review comments", async () => {
const payload = structuredClone(pullRequestReviewCommentPayload);
payload.comment.body = "Please rerun.\r\n@dcoapp recheck\r\nThanks!";

const mock = nock("https://api.github.com")
.get("/repos/robotland/test/contents/.github%2Fdco.yml")
.reply(404)
.get("/repos/robotland/.github/contents/.github%2Fdco.yml")
.reply(404)

.get(
"/repos/robotland/test/compare/607c64cd8e37eb2db939f99a17bee5c7d1a90a31...e76ed6025cec8879c75454a6efd6081d46de4c94"
)
.reply(200, compare)

.post("/repos/robotland/test/check-runs", (body) => {
body.started_at = "2018-07-14T18:18:54.156Z";
body.completed_at = "2018-07-14T18:18:54.156Z";
expect(body).toMatchObject({
conclusion: "action_required",
head_branch: "dco-test",
head_sha: "e76ed6025cec8879c75454a6efd6081d46de4c94",
name: "DCO",
output: {
title: "DCO",
},
status: "completed",
});
expect(body.output.summary).toContain("The sign-off is missing.");

return true;
})
.reply(200);

await probot.receive({ name: "pull_request_review_comment", payload });

expect(mock.activeMocks()).toStrictEqual([]);
});

test("checks org membership for require.members: false review comments", async () => {
const mock = nock("https://api.github.com")
.get("/repos/robotland/test/contents/.github%2Fdco.yml")
.reply(
200,
`
require:
members: false`
)

.get(
"/repos/robotland/test/compare/607c64cd8e37eb2db939f99a17bee5c7d1a90a31...e76ed6025cec8879c75454a6efd6081d46de4c94"
)
.reply(200, compare)

.get("/orgs/robotland/members/bkeepers")
.reply(204)

.post("/repos/robotland/test/check-runs", (body) => {
body.started_at = "2018-07-14T18:18:54.156Z";
body.completed_at = "2018-07-14T18:18:54.156Z";
expect(body).toMatchObject({
conclusion: "action_required",
head_branch: "dco-test",
head_sha: "e76ed6025cec8879c75454a6efd6081d46de4c94",
name: "DCO",
output: {
title: "DCO",
},
status: "completed",
});

return true;
})
.reply(200);

await probot.receive({
name: "pull_request_review_comment",
payload: pullRequestReviewCommentPayload,
});

expect(mock.activeMocks()).toStrictEqual([]);
});
Expand Down