Merge pull request #125 from github/prevent-duplicate-approvals
Prevent Duplicate Approvals
This commit is contained in:
Коммит
245059b04a
|
@ -6,6 +6,14 @@ import * as core from "@actions/core";
|
||||||
jest.spyOn(core, "debug").mockImplementation(() => {});
|
jest.spyOn(core, "debug").mockImplementation(() => {});
|
||||||
jest.spyOn(core, "info").mockImplementation(() => {});
|
jest.spyOn(core, "info").mockImplementation(() => {});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
nock("https://api.github.com")
|
||||||
|
.persist()
|
||||||
|
.get("/user")
|
||||||
|
.reply(200, { login: "octocat" });
|
||||||
|
});
|
||||||
|
|
||||||
test("It creates an approved review", async () => {
|
test("It creates an approved review", async () => {
|
||||||
process.env["GITHUB_REPOSITORY"] = "foo/bar";
|
process.env["GITHUB_REPOSITORY"] = "foo/bar";
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
const nock = require("nock");
|
||||||
import { GitHubProvider } from "../src/github-provider";
|
import { GitHubProvider } from "../src/github-provider";
|
||||||
import { PrivilegedRequester } from "../src/privileged-requester";
|
import { PrivilegedRequester } from "../src/privileged-requester";
|
||||||
|
|
||||||
|
@ -6,6 +7,14 @@ import * as core from "@actions/core";
|
||||||
jest.spyOn(core, "debug").mockImplementation(() => {});
|
jest.spyOn(core, "debug").mockImplementation(() => {});
|
||||||
jest.spyOn(core, "info").mockImplementation(() => {});
|
jest.spyOn(core, "info").mockImplementation(() => {});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
nock("https://api.github.com")
|
||||||
|
.persist()
|
||||||
|
.get("/user")
|
||||||
|
.reply(200, { login: "octocat" });
|
||||||
|
});
|
||||||
|
|
||||||
test("We receive the expected config content", async () => {
|
test("We receive the expected config content", async () => {
|
||||||
let configContent = `---
|
let configContent = `---
|
||||||
requesters:
|
requesters:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { GitHubProvider } from "../src/github-provider";
|
import { GitHubProvider } from "../src/github-provider";
|
||||||
import { PullRequest } from "../src/pull-request";
|
import { PullRequest } from "../src/pull-request";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
const nock = require("nock");
|
||||||
|
|
||||||
// jest spy on to silence output
|
// jest spy on to silence output
|
||||||
jest.spyOn(core, "info").mockImplementation(() => {});
|
jest.spyOn(core, "info").mockImplementation(() => {});
|
||||||
|
@ -10,13 +11,37 @@ jest.spyOn(core, "debug").mockImplementation(() => {});
|
||||||
jest.spyOn(core, "setFailed").mockImplementation(() => {});
|
jest.spyOn(core, "setFailed").mockImplementation(() => {});
|
||||||
jest.spyOn(core, "setOutput").mockImplementation(() => {});
|
jest.spyOn(core, "setOutput").mockImplementation(() => {});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
nock("https://api.github.com")
|
||||||
|
.persist()
|
||||||
|
.get("/user")
|
||||||
|
.reply(200, { login: "octocat" });
|
||||||
|
});
|
||||||
|
|
||||||
test("We can create a review", async () => {
|
test("We can create a review", async () => {
|
||||||
let provider = new GitHubProvider("token");
|
let provider = new GitHubProvider("token");
|
||||||
|
jest.spyOn(provider, "hasAlreadyApproved").mockImplementation(() => false);
|
||||||
jest.spyOn(provider, "createReview").mockImplementation(() => true);
|
jest.spyOn(provider, "createReview").mockImplementation(() => true);
|
||||||
expect(provider.createReview()).toBe(true);
|
expect(provider.createReview()).toBe(true);
|
||||||
|
|
||||||
let pullRequest = new PullRequest(provider);
|
let pullRequest = new PullRequest(provider);
|
||||||
let approval = await pullRequest.approve();
|
let approval = await pullRequest.approve();
|
||||||
|
expect(core.info).toHaveBeenCalledWith(
|
||||||
|
"Approving the PR for a privileged reviewer.",
|
||||||
|
);
|
||||||
|
expect(approval).toStrictEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("We attempt to create a review but we already approved in a previous workflow run", async () => {
|
||||||
|
let provider = new GitHubProvider("token");
|
||||||
|
jest.spyOn(provider, "hasAlreadyApproved").mockImplementation(() => true);
|
||||||
|
|
||||||
|
let pullRequest = new PullRequest(provider);
|
||||||
|
let approval = await pullRequest.approve();
|
||||||
|
expect(core.info).toHaveBeenCalledWith(
|
||||||
|
"PR has already been approved by this Action, skipping duplicate approval.",
|
||||||
|
);
|
||||||
expect(approval).toStrictEqual(undefined);
|
expect(approval).toStrictEqual(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,13 @@ import { PullRequest } from "../src/pull-request";
|
||||||
import { Runner } from "../src/runner";
|
import { Runner } from "../src/runner";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
|
const nock = require("nock");
|
||||||
|
|
||||||
|
nock("https://api.github.com")
|
||||||
|
.persist()
|
||||||
|
.get("/user")
|
||||||
|
.reply(200, { login: "octocat" });
|
||||||
|
|
||||||
let provider = new GitHubProvider("token");
|
let provider = new GitHubProvider("token");
|
||||||
let pullRequest = new PullRequest(provider);
|
let pullRequest = new PullRequest(provider);
|
||||||
let runner = new Runner(pullRequest);
|
let runner = new Runner(pullRequest);
|
||||||
|
|
|
@ -37664,6 +37664,49 @@ class GitHubProvider {
|
||||||
this.configContent = false;
|
this.configContent = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCurrentUser() {
|
||||||
|
const { data: currentUser } =
|
||||||
|
await this.octokit.rest.users.getAuthenticated();
|
||||||
|
return currentUser.login;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the current authenticated user (login) has an active APPROVED review on the PR
|
||||||
|
// returns true if the user has an active APPROVED review, false otherwise
|
||||||
|
// note: if the user had an active APPROVED review but it was dismissed, this will return false
|
||||||
|
async hasAlreadyApproved(prNumber) {
|
||||||
|
// get the login of the current authenticated user
|
||||||
|
const login = await this.getCurrentUser();
|
||||||
|
core.info(
|
||||||
|
`checking if ${login} has already approved PR #${prNumber} in a previous workflow run`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: reviews } = await this.octokit.rest.pulls.listReviews({
|
||||||
|
owner: github.context.repo.owner,
|
||||||
|
repo: github.context.repo.repo,
|
||||||
|
pull_number: prNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
// filter out all reviews that are not APPROVED
|
||||||
|
const approvedReviews = reviews.filter(
|
||||||
|
(review) => review.state === "APPROVED",
|
||||||
|
);
|
||||||
|
|
||||||
|
// filter out all reviews that are not by the current authenticated user via login
|
||||||
|
const approvedReviewsByUser = approvedReviews.filter(
|
||||||
|
(review) => review.user.login.toLowerCase() === login.toLowerCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const approved = approvedReviewsByUser.length > 0;
|
||||||
|
if (approved) {
|
||||||
|
core.info(
|
||||||
|
`${login} has already approved PR #${prNumber} in a previous workflow run`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there are any reviews left, then login (this Action) has already approved the PR and we should not approve it again
|
||||||
|
return approved;
|
||||||
|
}
|
||||||
|
|
||||||
async createReview(prNumber, reviewEvent) {
|
async createReview(prNumber, reviewEvent) {
|
||||||
core.debug(`prNumber: ${prNumber}`);
|
core.debug(`prNumber: ${prNumber}`);
|
||||||
core.debug(`reviewEvent: ${reviewEvent}`);
|
core.debug(`reviewEvent: ${reviewEvent}`);
|
||||||
|
@ -37784,6 +37827,15 @@ class PullRequest {
|
||||||
|
|
||||||
async approve() {
|
async approve() {
|
||||||
try {
|
try {
|
||||||
|
// before we approved the PR, check to see if this workflow has already approved the PR in a previous run
|
||||||
|
if (await this.github.hasAlreadyApproved(this.prNumber)) {
|
||||||
|
lib_core.info(
|
||||||
|
"PR has already been approved by this Action, skipping duplicate approval.",
|
||||||
|
);
|
||||||
|
lib_core.setOutput("approved", "true"); // set to true as we have already approved the PR at some point
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
lib_core.info("Approving the PR for a privileged reviewer.");
|
lib_core.info("Approving the PR for a privileged reviewer.");
|
||||||
await this.github.createReview(this.prNumber, "APPROVE");
|
await this.github.createReview(this.prNumber, "APPROVE");
|
||||||
lib_core.info("PR approved, all set!");
|
lib_core.info("PR approved, all set!");
|
||||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -10,6 +10,49 @@ class GitHubProvider {
|
||||||
this.configContent = false;
|
this.configContent = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCurrentUser() {
|
||||||
|
const { data: currentUser } =
|
||||||
|
await this.octokit.rest.users.getAuthenticated();
|
||||||
|
return currentUser.login;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the current authenticated user (login) has an active APPROVED review on the PR
|
||||||
|
// returns true if the user has an active APPROVED review, false otherwise
|
||||||
|
// note: if the user had an active APPROVED review but it was dismissed, this will return false
|
||||||
|
async hasAlreadyApproved(prNumber) {
|
||||||
|
// get the login of the current authenticated user
|
||||||
|
const login = await this.getCurrentUser();
|
||||||
|
core.info(
|
||||||
|
`checking if ${login} has already approved PR #${prNumber} in a previous workflow run`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: reviews } = await this.octokit.rest.pulls.listReviews({
|
||||||
|
owner: github.context.repo.owner,
|
||||||
|
repo: github.context.repo.repo,
|
||||||
|
pull_number: prNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
// filter out all reviews that are not APPROVED
|
||||||
|
const approvedReviews = reviews.filter(
|
||||||
|
(review) => review.state === "APPROVED",
|
||||||
|
);
|
||||||
|
|
||||||
|
// filter out all reviews that are not by the current authenticated user via login
|
||||||
|
const approvedReviewsByUser = approvedReviews.filter(
|
||||||
|
(review) => review.user.login.toLowerCase() === login.toLowerCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const approved = approvedReviewsByUser.length > 0;
|
||||||
|
if (approved) {
|
||||||
|
core.info(
|
||||||
|
`${login} has already approved PR #${prNumber} in a previous workflow run`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there are any reviews left, then login (this Action) has already approved the PR and we should not approve it again
|
||||||
|
return approved;
|
||||||
|
}
|
||||||
|
|
||||||
async createReview(prNumber, reviewEvent) {
|
async createReview(prNumber, reviewEvent) {
|
||||||
core.debug(`prNumber: ${prNumber}`);
|
core.debug(`prNumber: ${prNumber}`);
|
||||||
core.debug(`reviewEvent: ${reviewEvent}`);
|
core.debug(`reviewEvent: ${reviewEvent}`);
|
||||||
|
|
|
@ -15,6 +15,15 @@ class PullRequest {
|
||||||
|
|
||||||
async approve() {
|
async approve() {
|
||||||
try {
|
try {
|
||||||
|
// before we approved the PR, check to see if this workflow has already approved the PR in a previous run
|
||||||
|
if (await this.github.hasAlreadyApproved(this.prNumber)) {
|
||||||
|
core.info(
|
||||||
|
"PR has already been approved by this Action, skipping duplicate approval.",
|
||||||
|
);
|
||||||
|
core.setOutput("approved", "true"); // set to true as we have already approved the PR at some point
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
core.info("Approving the PR for a privileged reviewer.");
|
core.info("Approving the PR for a privileged reviewer.");
|
||||||
await this.github.createReview(this.prNumber, "APPROVE");
|
await this.github.createReview(this.prNumber, "APPROVE");
|
||||||
core.info("PR approved, all set!");
|
core.info("PR approved, all set!");
|
||||||
|
|
Загрузка…
Ссылка в новой задаче