From 366d40d410becaba63b47cbb16383ddc876e1831 Mon Sep 17 00:00:00 2001 From: Vince Jerald Villamora Date: Thu, 20 Oct 2022 05:45:20 +0800 Subject: [PATCH] feat: Add optional input `force-review`, fixes #206 When force-review is true, re-approves the pull request even if current review state is already approved --- action.yml | 3 + dist/index.js | 446 +++++++++++++++++++++++--------------------- src/approve.test.ts | 28 +++ src/approve.ts | 22 ++- src/main.test.ts | 34 +++- src/main.ts | 10 +- 6 files changed, 317 insertions(+), 226 deletions(-) diff --git a/action.yml b/action.yml index 301cb84..202d2ec 100644 --- a/action.yml +++ b/action.yml @@ -14,6 +14,9 @@ inputs: review-message: description: '(optional) The message of the pull request review.' required: false + force-review: + description: '(optional) Re-approves the PR even if status is already approved. Used for cases when a PR requests for a re-review.' + required: false runs: using: 'node16' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index 192a953..0c0da53 100644 --- a/dist/index.js +++ b/dist/index.js @@ -10031,149 +10031,157 @@ function wrappy (fn, cb) { /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; - -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.approve = void 0; -const core = __importStar(__nccwpck_require__(2186)); -const github = __importStar(__nccwpck_require__(5438)); -const request_error_1 = __nccwpck_require__(537); -function approve(token, context, prNumber, reviewMessage) { - 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; - } - if (!prNumber) { - core.setFailed("Event payload missing `pull_request` key, and no `pull-request-number` provided as input." + - "Make sure you're triggering this action on the `pull_request` or `pull_request_target` events."); - return; - } - const client = github.getOctokit(token); - try { - core.info(`Getting current user info`); - const login = yield getLoginForToken(client); - core.info(`Current user is ${login}`); - core.info(`Getting pull request #${prNumber} info`); - const pull_request = yield client.rest.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.rest.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) == 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.rest.pulls.createReview({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: prNumber, - body: reviewMessage, - event: "APPROVE", - }); - core.info(`Approved pull request #${prNumber}`); - } - catch (error) { - if (error instanceof request_error_1.RequestError) { - switch (error.status) { - case 401: - core.setFailed(`${error.message}. Please check that the \`github-token\` input ` + - "parameter is set correctly."); - break; - case 403: - core.setFailed(`${error.message}. In some cases, the GitHub token used for actions triggered ` + - "from `pull_request` events are read-only, which can cause this problem. " + - "Switching to the `pull_request_target` event typically resolves this issue."); - break; - case 404: - core.setFailed(`${error.message}. This typically means the token you're using doesn't have ` + - "access to this repository. Use the built-in `${{ secrets.GITHUB_TOKEN }}` token " + - "or review the scopes assigned to your personal access token."); - break; - case 422: - core.setFailed(`${error.message}. This typically happens when you try to approve the pull ` + - "request with the same user account that created the pull request. Try using " + - "the built-in `${{ secrets.GITHUB_TOKEN }}` token, or if you're using a personal " + - "access token, use one that belongs to a dedicated bot account."); - break; - default: - core.setFailed(`Error (code ${error.status}): ${error.message}`); - } - return; - } - if (error instanceof Error) { - core.setFailed(error); - } - else { - core.setFailed("Unknown error"); - } - return; - } - }); -} -exports.approve = approve; -function getLoginForToken(client) { - return __awaiter(this, void 0, void 0, function* () { - try { - const { data: user } = yield client.rest.users.getAuthenticated(); - return user.login; - } - catch (error) { - if (error instanceof request_error_1.RequestError) { - // If you use the GITHUB_TOKEN provided by GitHub Actions to fetch the current user - // you get a 403. For now we'll assume any 403 means this is an Actions token. - if (error.status === 403) { - return "github-actions[bot]"; - } - } - throw error; - } - }); -} + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.approve = void 0; +const core = __importStar(__nccwpck_require__(2186)); +const github = __importStar(__nccwpck_require__(5438)); +const request_error_1 = __nccwpck_require__(537); +function approve(token, context, prNumber, reviewMessage, forceReview) { + 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; + } + if (forceReview == null) { + forceReview = false; + } + if (!prNumber) { + core.setFailed("Event payload missing `pull_request` key, and no `pull-request-number` provided as input." + + "Make sure you're triggering this action on the `pull_request` or `pull_request_target` events."); + return; + } + const client = github.getOctokit(token); + try { + core.info(`Getting current user info`); + const login = yield getLoginForToken(client); + core.info(`Current user is ${login}`); + core.info(`Getting pull request #${prNumber} info`); + const pull_request = yield client.rest.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.rest.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) == login && + review.commit_id == commit && + review.state == "APPROVED") { + if (forceReview) { + core.info(`Current user already approved pull request #${prNumber}, but forceReview is set to true, so re-approving anyway`); + } + else { + core.info(`Current user already approved pull request #${prNumber}, nothing to do`); + return; + } + } + } + core.info(`Creating approving review for pull request #${prNumber}`); + yield client.rest.pulls.createReview({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + body: reviewMessage, + event: "APPROVE", + }); + core.info(`Approved pull request #${prNumber}`); + } + catch (error) { + if (error instanceof request_error_1.RequestError) { + switch (error.status) { + case 401: + core.setFailed(`${error.message}. Please check that the \`github-token\` input ` + + "parameter is set correctly."); + break; + case 403: + core.setFailed(`${error.message}. In some cases, the GitHub token used for actions triggered ` + + "from `pull_request` events are read-only, which can cause this problem. " + + "Switching to the `pull_request_target` event typically resolves this issue."); + break; + case 404: + core.setFailed(`${error.message}. This typically means the token you're using doesn't have ` + + "access to this repository. Use the built-in `${{ secrets.GITHUB_TOKEN }}` token " + + "or review the scopes assigned to your personal access token."); + break; + case 422: + core.setFailed(`${error.message}. This typically happens when you try to approve the pull ` + + "request with the same user account that created the pull request. Try using " + + "the built-in `${{ secrets.GITHUB_TOKEN }}` token, or if you're using a personal " + + "access token, use one that belongs to a dedicated bot account."); + break; + default: + core.setFailed(`Error (code ${error.status}): ${error.message}`); + } + return; + } + if (error instanceof Error) { + core.setFailed(error); + } + else { + core.setFailed("Unknown error"); + } + return; + } + }); +} +exports.approve = approve; +function getLoginForToken(client) { + return __awaiter(this, void 0, void 0, function* () { + try { + const { data: user } = yield client.rest.users.getAuthenticated(); + return user.login; + } + catch (error) { + if (error instanceof request_error_1.RequestError) { + // If you use the GITHUB_TOKEN provided by GitHub Actions to fetch the current user + // you get a 403. For now we'll assume any 403 means this is an Actions token. + if (error.status === 403) { + return "github-actions[bot]"; + } + } + throw error; + } + }); +} /***/ }), @@ -10182,79 +10190,85 @@ function getLoginForToken(client) { /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; - -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.run = void 0; -const core = __importStar(__nccwpck_require__(2186)); -const github = __importStar(__nccwpck_require__(5438)); -const approve_1 = __nccwpck_require__(6609); -function run() { - return __awaiter(this, void 0, void 0, function* () { - try { - const token = core.getInput("github-token"); - const reviewMessage = core.getInput("review-message"); - yield (0, approve_1.approve)(token, github.context, prNumber(), reviewMessage || undefined); - } - catch (error) { - if (error instanceof Error) { - core.setFailed(error.message); - } - else { - core.setFailed("Unknown error"); - } - } - }); -} -exports.run = run; -function prNumber() { - if (core.getInput("pull-request-number") !== "") { - const prNumber = parseInt(core.getInput("pull-request-number"), 10); - if (Number.isNaN(prNumber)) { - throw new Error("Invalid `pull-request-number` value"); - } - return prNumber; - } - if (!github.context.payload.pull_request) { - throw new Error("This action must be run using a `pull_request` event or " + - "have an explicit `pull-request-number` provided"); - } - return github.context.payload.pull_request.number; -} -if (require.main === require.cache[eval('__filename')]) { - run(); -} + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.run = void 0; +const core = __importStar(__nccwpck_require__(2186)); +const github = __importStar(__nccwpck_require__(5438)); +const approve_1 = __nccwpck_require__(6609); +function run() { + return __awaiter(this, void 0, void 0, function* () { + try { + const token = core.getInput("github-token"); + const reviewMessage = core.getInput("review-message"); + yield (0, approve_1.approve)(token, github.context, prNumber(), reviewMessage || undefined, forceReview()); + } + catch (error) { + if (error instanceof Error) { + core.setFailed(error.message); + } + else { + core.setFailed("Unknown error"); + } + } + }); +} +exports.run = run; +function prNumber() { + if (core.getInput("pull-request-number") !== "") { + const prNumber = parseInt(core.getInput("pull-request-number"), 10); + if (Number.isNaN(prNumber)) { + throw new Error("Invalid `pull-request-number` value"); + } + return prNumber; + } + if (!github.context.payload.pull_request) { + throw new Error("This action must be run using a `pull_request` event or " + + "have an explicit `pull-request-number` provided"); + } + return github.context.payload.pull_request.number; +} +function forceReview() { + if (core.getInput("force-review") === undefined) { + return false; + } + return (/true/i).test(core.getInput("force-review")); +} +if (require.main === require.cache[eval('__filename')]) { + run(); +} /***/ }), diff --git a/src/approve.test.ts b/src/approve.test.ts index 1f5a2c7..5c591e7 100644 --- a/src/approve.test.ts +++ b/src/approve.test.ts @@ -302,6 +302,34 @@ test("when a review has already been approved by another user", async () => { ); }); +test("when a review has already been approved by another user and forceReview is set to true", 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, undefined, true); + + 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" }); diff --git a/src/approve.ts b/src/approve.ts index 8e4f20d..ea1982a 100644 --- a/src/approve.ts +++ b/src/approve.ts @@ -8,11 +8,15 @@ export async function approve( token: string, context: Context, prNumber?: number, - reviewMessage?: string + reviewMessage?: string, + forceReview?: boolean, ) { if (!prNumber) { prNumber = context.payload.pull_request?.number; } + if (!forceReview) { + forceReview = false; + } if (!prNumber) { core.setFailed( @@ -54,15 +58,21 @@ export async function approve( review.commit_id == commit && review.state == "APPROVED" ) { - core.info( - `Current user already approved pull request #${prNumber}, nothing to do` - ); - return; + if (forceReview) { + core.info( + `Current user already approved pull request #${prNumber}, but forceReview is set to true, so re-approving anyway` + ) + } else { + 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` + `Creating approving review for pull request #${prNumber}` ); await client.rest.pulls.createReview({ owner: context.repo.owner, diff --git a/src/main.test.ts b/src/main.test.ts index e3f05ba..759fec5 100644 --- a/src/main.test.ts +++ b/src/main.test.ts @@ -43,7 +43,8 @@ test("passes the review message to approve", async () => { "tok-xyz", expect.anything(), 101, - "LGTM" + "LGTM", + expect.anything() ); }); @@ -54,7 +55,8 @@ test("calls approve when no PR number is provided", async () => { "tok-xyz", expect.anything(), 101, - undefined + undefined, + expect.anything() ); }); @@ -65,7 +67,8 @@ test("calls approve when a valid PR number is provided", async () => { "tok-xyz", expect.anything(), 456, - undefined + undefined, + expect.anything() ); }); @@ -75,6 +78,31 @@ test("errors when an invalid PR number is provided", async () => { expect(mockedApprove).not.toHaveBeenCalled(); }); +test("calls approve when force-review is set to true", async () => { + process.env["INPUT_PULL-REQUEST-NUMBER"] = "456"; + process.env["INPUT_FORCE-REVIEW"] = "true"; + await run(); + expect(mockedApprove).toHaveBeenCalledWith( + "tok-xyz", + expect.anything(), + 456, + undefined, + true + ); +}); + +test("calls approve when force-review is set to false", async () => { + process.env["INPUT_PULL-REQUEST-NUMBER"] = "456"; + await run(); + expect(mockedApprove).toHaveBeenCalledWith( + "tok-xyz", + expect.anything(), + 456, + undefined, + false + ); +}); + function ghContext(): Context { const ctx = new Context(); ctx.payload = { diff --git a/src/main.ts b/src/main.ts index 42cee34..25cd263 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,7 +10,8 @@ export async function run() { token, github.context, prNumber(), - reviewMessage || undefined + reviewMessage || undefined, + forceReview() ); } catch (error) { if (error instanceof Error) { @@ -39,6 +40,13 @@ function prNumber(): number { return github.context.payload.pull_request.number; } +function forceReview(): boolean { + if (core.getInput("force-review") === undefined) { + return false; + } + return (/true/i).test(core.getInput("force-review")) +} + if (require.main === module) { run(); }