Merge pull request #125 from github/prevent-duplicate-approvals

Prevent Duplicate Approvals
This commit is contained in:
Grant Birkinbine 2023-11-28 13:02:49 -07:00 коммит произвёл GitHub
Родитель fb0f25c143 e9094612a4
Коммит 245059b04a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 154 добавлений и 1 удалений

Просмотреть файл

@ -6,6 +6,14 @@ import * as core from "@actions/core";
jest.spyOn(core, "debug").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 () => {
process.env["GITHUB_REPOSITORY"] = "foo/bar";

Просмотреть файл

@ -1,3 +1,4 @@
const nock = require("nock");
import { GitHubProvider } from "../src/github-provider";
import { PrivilegedRequester } from "../src/privileged-requester";
@ -6,6 +7,14 @@ import * as core from "@actions/core";
jest.spyOn(core, "debug").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 () => {
let configContent = `---
requesters:

Просмотреть файл

@ -1,6 +1,7 @@
import { GitHubProvider } from "../src/github-provider";
import { PullRequest } from "../src/pull-request";
import * as core from "@actions/core";
const nock = require("nock");
// jest spy on to silence output
jest.spyOn(core, "info").mockImplementation(() => {});
@ -10,13 +11,37 @@ jest.spyOn(core, "debug").mockImplementation(() => {});
jest.spyOn(core, "setFailed").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 () => {
let provider = new GitHubProvider("token");
jest.spyOn(provider, "hasAlreadyApproved").mockImplementation(() => false);
jest.spyOn(provider, "createReview").mockImplementation(() => true);
expect(provider.createReview()).toBe(true);
let pullRequest = new PullRequest(provider);
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);
});

Просмотреть файл

@ -4,6 +4,13 @@ import { PullRequest } from "../src/pull-request";
import { Runner } from "../src/runner";
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 pullRequest = new PullRequest(provider);
let runner = new Runner(pullRequest);

52
dist/index.js сгенерированный поставляемый
Просмотреть файл

@ -37664,6 +37664,49 @@ class GitHubProvider {
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) {
core.debug(`prNumber: ${prNumber}`);
core.debug(`reviewEvent: ${reviewEvent}`);
@ -37784,6 +37827,15 @@ class PullRequest {
async approve() {
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.");
await this.github.createReview(this.prNumber, "APPROVE");
lib_core.info("PR approved, all set!");

2
dist/index.js.map сгенерированный поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -10,6 +10,49 @@ class GitHubProvider {
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) {
core.debug(`prNumber: ${prNumber}`);
core.debug(`reviewEvent: ${reviewEvent}`);

Просмотреть файл

@ -15,6 +15,15 @@ class PullRequest {
async approve() {
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.");
await this.github.createReview(this.prNumber, "APPROVE");
core.info("PR approved, all set!");