diff --git a/dist/index.js b/dist/index.js index 01ae5f8..4b833b9 100644 --- a/dist/index.js +++ b/dist/index.js @@ -5867,7 +5867,7 @@ const core = __importStar(__nccwpck_require__(186)); const github = __importStar(__nccwpck_require__(438)); const request_error_1 = __nccwpck_require__(537); function approve(token, context, prNumber) { - var _a; + var _a, _b; return __awaiter(this, void 0, void 0, function* () { if (!prNumber) { prNumber = (_a = context.payload.pull_request) === null || _a === void 0 ? void 0 : _a.number; @@ -5878,8 +5878,33 @@ function approve(token, context, prNumber) { return; } const client = github.getOctokit(token); - core.info(`Creating approving review for pull request #${prNumber}`); try { + core.info(`Getting current user info`); + const { data: user } = yield client.users.getAuthenticated(); + core.info(`Current user is ${user.login}`); + core.info(`Getting pull request #${prNumber} info`); + const pull_request = yield client.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + const commit = pull_request.data.head.sha; + core.info(`Commit SHA is ${commit}`); + core.info(`Getting reviews for pull request #${prNumber} and commit ${commit}`); + const reviews = yield client.pulls.listReviews({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + for (const review of reviews.data) { + if (((_b = review.user) === null || _b === void 0 ? void 0 : _b.login) == user.login && + review.commit_id == commit && + review.state == "APPROVED") { + core.info(`Current user already approved pull request #${prNumber}, nothing to do`); + return; + } + } + core.info(`Pull request #${prNumber} has not been approved yet, creating approving review`); yield client.pulls.createReview({ owner: context.repo.owner, repo: context.repo.repo, diff --git a/src/approve.test.ts b/src/approve.test.ts index e20e115..cc797e7 100644 --- a/src/approve.test.ts +++ b/src/approve.test.ts @@ -3,15 +3,34 @@ import { Context } from "@actions/github/lib/context"; import nock from "nock"; import { approve } from "./approve"; +const originalEnv = process.env; + beforeEach(() => { jest.restoreAllMocks(); jest.spyOn(core, "setFailed").mockImplementation(jest.fn()); jest.spyOn(core, "info").mockImplementation(jest.fn()); + nock.disableNetConnect(); + + process.env = { GITHUB_REPOSITORY: "hmarr/test" }; +}); - process.env["GITHUB_REPOSITORY"] = "hmarr/test"; +afterEach(() => { + nock.cleanAll(); + nock.enableNetConnect(); + process.env = originalEnv; }); test("when a review is successfully created", async () => { + nock("https://api.github.com").get("/user").reply(200, { login: "hmarr" }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101") + .reply(200, { head: { sha: "24c5451bbf1fb09caa3ac8024df4788aff4d4974" } }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101/reviews") + .reply(200, []); + nock("https://api.github.com") .post("/repos/hmarr/test/pulls/101/reviews") .reply(200, { id: 1 }); @@ -24,6 +43,238 @@ test("when a review is successfully created", async () => { }); test("when a review is successfully created using pull-request-number", async () => { + nock("https://api.github.com").get("/user").reply(200, { login: "hmarr" }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/102") + .reply(200, { head: { sha: "24c5451bbf1fb09caa3ac8024df4788aff4d4974" } }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/102/reviews") + .reply(200, []); + + nock("https://api.github.com") + .post("/repos/hmarr/test/pulls/102/reviews") + .reply(200, { id: 1 }); + + await approve("gh-tok", new Context(), 102); + + expect(core.info).toHaveBeenCalledWith( + expect.stringContaining("Approved pull request #102") + ); +}); + +test("when a review has already been approved by current user", async () => { + nock("https://api.github.com").get("/user").reply(200, { login: "hmarr" }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101") + .reply(200, { head: { sha: "24c5451bbf1fb09caa3ac8024df4788aff4d4974" } }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101/reviews") + .reply(200, [ + { + user: { login: "hmarr" }, + commit_id: "24c5451bbf1fb09caa3ac8024df4788aff4d4974", + state: "APPROVED", + }, + ]); + + await approve("gh-tok", ghContext()); + + expect(core.info).toHaveBeenCalledWith( + expect.stringContaining( + "Current user already approved pull request #101, nothing to do" + ) + ); +}); + +test("when a review is pending", async () => { + nock("https://api.github.com").get("/user").reply(200, { login: "hmarr" }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101") + .reply(200, { head: { sha: "24c5451bbf1fb09caa3ac8024df4788aff4d4974" } }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101/reviews") + .reply(200, [ + { + user: { login: "hmarr" }, + commit_id: "24c5451bbf1fb09caa3ac8024df4788aff4d4974", + state: "PENDING", + }, + ]); + + nock("https://api.github.com") + .post("/repos/hmarr/test/pulls/101/reviews") + .reply(200, { id: 1 }); + + await approve("gh-tok", new Context(), 101); + + expect(core.info).toHaveBeenCalledWith( + expect.stringContaining("Approved pull request #101") + ); +}); + +test("when a review is dismissed", async () => { + nock("https://api.github.com").get("/user").reply(200, { login: "hmarr" }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101") + .reply(200, { head: { sha: "24c5451bbf1fb09caa3ac8024df4788aff4d4974" } }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101/reviews") + .reply(200, [ + { + user: { login: "hmarr" }, + commit_id: "24c5451bbf1fb09caa3ac8024df4788aff4d4974", + state: "DISMISSED", + }, + ]); + + nock("https://api.github.com") + .post("/repos/hmarr/test/pulls/101/reviews") + .reply(200, { id: 1 }); + + await approve("gh-tok", new Context(), 101); + + expect(core.info).toHaveBeenCalledWith( + expect.stringContaining("Approved pull request #101") + ); +}); + +test("when a review is not approved", async () => { + nock("https://api.github.com").get("/user").reply(200, { login: "hmarr" }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101") + .reply(200, { head: { sha: "24c5451bbf1fb09caa3ac8024df4788aff4d4974" } }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101/reviews") + .reply(200, [ + { + user: { login: "hmarr" }, + commit_id: "24c5451bbf1fb09caa3ac8024df4788aff4d4974", + state: "CHANGES_REQUESTED", + }, + ]); + + nock("https://api.github.com") + .post("/repos/hmarr/test/pulls/101/reviews") + .reply(200, { id: 1 }); + + await approve("gh-tok", new Context(), 101); + + expect(core.info).toHaveBeenCalledWith( + expect.stringContaining("Approved pull request #101") + ); +}); + +test("when a review is commented", async () => { + nock("https://api.github.com").get("/user").reply(200, { login: "hmarr" }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101") + .reply(200, { head: { sha: "24c5451bbf1fb09caa3ac8024df4788aff4d4974" } }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101/reviews") + .reply(200, [ + { + user: { login: "hmarr" }, + commit_id: "24c5451bbf1fb09caa3ac8024df4788aff4d4974", + state: "COMMENTED", + }, + ]); + + nock("https://api.github.com") + .post("/repos/hmarr/test/pulls/101/reviews") + .reply(200, { id: 1 }); + + await approve("gh-tok", new Context(), 101); + + expect(core.info).toHaveBeenCalledWith( + expect.stringContaining("Approved pull request #101") + ); +}); + +test("when an old commit has already been approved", async () => { + nock("https://api.github.com").get("/user").reply(200, { login: "hmarr" }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101") + .reply(200, { head: { sha: "24c5451bbf1fb09caa3ac8024df4788aff4d4974" } }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101/reviews") + .reply(200, [ + { + user: { login: "hmarr" }, + commit_id: "6a9ec7556f0a7fa5b49527a1eea4878b8a22d2e0", + state: "APPROVED", + }, + ]); + + nock("https://api.github.com") + .post("/repos/hmarr/test/pulls/101/reviews") + .reply(200, { id: 1 }); + + await approve("gh-tok", ghContext()); + + expect(core.info).toHaveBeenCalledWith( + expect.stringContaining("Approved pull request #101") + ); +}); + +test("when a review has already been approved by another user", async () => { + nock("https://api.github.com").get("/user").reply(200, { login: "hmarr" }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101") + .reply(200, { head: { sha: "24c5451bbf1fb09caa3ac8024df4788aff4d4974" } }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101/reviews") + .reply(200, [ + { + user: { login: "some" }, + commit_id: "24c5451bbf1fb09caa3ac8024df4788aff4d4974", + state: "APPROVED", + }, + ]); + + nock("https://api.github.com") + .post("/repos/hmarr/test/pulls/101/reviews") + .reply(200, { id: 1 }); + + await approve("gh-tok", new Context(), 101); + + expect(core.info).toHaveBeenCalledWith( + expect.stringContaining("Approved pull request #101") + ); +}); + +test("when a review has already been approved by unknown user", async () => { + nock("https://api.github.com").get("/user").reply(200, { login: "hmarr" }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101") + .reply(200, { head: { sha: "24c5451bbf1fb09caa3ac8024df4788aff4d4974" } }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101/reviews") + .reply(200, [ + { + user: null, + commit_id: "24c5451bbf1fb09caa3ac8024df4788aff4d4974", + state: "APPROVED", + }, + ]); + nock("https://api.github.com") .post("/repos/hmarr/test/pulls/101/reviews") .reply(200, { id: 1 }); @@ -45,7 +296,7 @@ test("without a pull request", async () => { test("when the token is invalid", async () => { nock("https://api.github.com") - .post("/repos/hmarr/test/pulls/101/reviews") + .get("/user") .reply(401, { message: "Bad credentials" }); await approve("gh-tok", ghContext()); @@ -56,6 +307,16 @@ test("when the token is invalid", async () => { }); test("when the token doesn't have write permissions", async () => { + nock("https://api.github.com").get("/user").reply(200, { login: "hmarr" }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101") + .reply(200, { head: { sha: "24c5451bbf1fb09caa3ac8024df4788aff4d4974" } }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101/reviews") + .reply(200, []); + nock("https://api.github.com") .post("/repos/hmarr/test/pulls/101/reviews") .reply(403, { message: "Resource not accessible by integration" }); @@ -68,6 +329,16 @@ test("when the token doesn't have write permissions", async () => { }); test("when a user tries to approve their own pull request", async () => { + nock("https://api.github.com").get("/user").reply(200, { login: "hmarr" }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101") + .reply(200, { head: { sha: "24c5451bbf1fb09caa3ac8024df4788aff4d4974" } }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101/reviews") + .reply(200, []); + nock("https://api.github.com") .post("/repos/hmarr/test/pulls/101/reviews") .reply(422, { message: "Unprocessable Entity" }); @@ -79,7 +350,67 @@ test("when a user tries to approve their own pull request", async () => { ); }); -test("when the token doesn't have access to the repository", async () => { +test("when pull request does not exist", async () => { + nock("https://api.github.com").get("/user").reply(200, { login: "hmarr" }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101") + .reply(404, { message: "Not Found" }); + + await approve("gh-tok", ghContext()); + + expect(core.setFailed).toHaveBeenCalledWith( + expect.stringContaining("doesn't have access") + ); +}); + +test("when the token doesn't have read access to the repository", async () => { + nock("https://api.github.com").get("/user").reply(200, { login: "hmarr" }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101") + .reply(404, { message: "Not Found" }); + + await approve("gh-tok", ghContext()); + + expect(core.setFailed).toHaveBeenCalledWith( + expect.stringContaining("doesn't have access") + ); +}); + +test("when the token is read-only", async () => { + nock("https://api.github.com").get("/user").reply(200, { login: "hmarr" }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101") + .reply(200, { head: { sha: "24c5451bbf1fb09caa3ac8024df4788aff4d4974" } }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101/reviews") + .reply(200, []); + + nock("https://api.github.com") + .post("/repos/hmarr/test/pulls/101/reviews") + .reply(403, { message: "Not Authorized" }); + + await approve("gh-tok", ghContext()); + + expect(core.setFailed).toHaveBeenCalledWith( + expect.stringContaining("are read-only") + ); +}); + +test("when the token doesn't have write access to the repository", async () => { + nock("https://api.github.com").get("/user").reply(200, { login: "hmarr" }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101") + .reply(200, { head: { sha: "24c5451bbf1fb09caa3ac8024df4788aff4d4974" } }); + + nock("https://api.github.com") + .get("/repos/hmarr/test/pulls/101/reviews") + .reply(200, []); + nock("https://api.github.com") .post("/repos/hmarr/test/pulls/101/reviews") .reply(404, { message: "Not Found" }); diff --git a/src/approve.ts b/src/approve.ts index bf94340..9c2eb38 100644 --- a/src/approve.ts +++ b/src/approve.ts @@ -22,8 +22,46 @@ export async function approve( const client = github.getOctokit(token); - core.info(`Creating approving review for pull request #${prNumber}`); try { + core.info(`Getting current user info`); + const { data: user } = await client.users.getAuthenticated(); + core.info(`Current user is ${user.login}`); + + core.info(`Getting pull request #${prNumber} info`); + const pull_request = await client.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + const commit = pull_request.data.head.sha; + + core.info(`Commit SHA is ${commit}`); + + core.info( + `Getting reviews for pull request #${prNumber} and commit ${commit}` + ); + const reviews = await client.pulls.listReviews({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + + for (const review of reviews.data) { + if ( + review.user?.login == user.login && + review.commit_id == commit && + review.state == "APPROVED" + ) { + core.info( + `Current user already approved pull request #${prNumber}, nothing to do` + ); + return; + } + } + + core.info( + `Pull request #${prNumber} has not been approved yet, creating approving review` + ); await client.pulls.createReview({ owner: context.repo.owner, repo: context.repo.repo, @@ -67,7 +105,6 @@ export async function approve( } return; } - core.setFailed(error.message); return; }