From aa910dc5205c720fdf4d3c6079843c91afaa4881 Mon Sep 17 00:00:00 2001 From: Kyle Harding Date: Thu, 5 Dec 2024 14:07:23 -0500 Subject: [PATCH] Reject reviews by commit authors and/or committers Testing confirmed that the deployment creator property will reflect whoever triggered the workflow, including performing actions like merging a PR, and does not reflect the commit author. Instead, compare review authors and bypass users against the author of the commit that triggered the event. Change-type: patch Signed-off-by: Kyle Harding --- README.md | 8 +- src/client.ts | 11 + src/handlers/deployment-protection-rule.ts | 22 +- src/handlers/pull-request-review.ts | 75 ++- .../pull_request_review_comment.created.json | 600 ------------------ .../deployment-protection-rule.test.ts | 72 ++- test/handlers/pull-request-review.test.ts | 45 +- 7 files changed, 180 insertions(+), 653 deletions(-) delete mode 100644 test/fixtures/pull_request_review_comment.created.json diff --git a/README.md b/README.md index bc80da0..397b034 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ Deployments are approved by submitting a review with the `/deploy` command. Deployments are auto-approved if: -- They originate from a previously approved commit SHA (including pull request merges). +- They triggered by a previously approved commit SHA (including pull request merges). - They are initiated by an allowlisted user (e.g., Renovate) who is: - - The deployment creator. + - The author of the commit that triggered the deployment. - Listed in the `BYPASS_USERS` IDs. #### Manual Approval Process @@ -38,7 +38,7 @@ For manual approvals: Reviewers must: - Have repository write access or higher. -- Not be the deployment actor. +- Not be the commit author or committer. - Not be a bot account. ### Security Measures @@ -49,7 +49,7 @@ Key security features include: - Ensuring comment integrity (unmodified since creation). - Maintaining stateless operations. - Preventing TOCTOU attacks with atomic operations. -- Requiring different actors for deployment and approval. +- Requiring different actors for commit and approval. ### Example Workflow diff --git a/src/client.ts b/src/client.ts index a2663c4..d5933ef 100644 --- a/src/client.ts +++ b/src/client.ts @@ -5,6 +5,7 @@ import type { } from '@octokit/webhooks-types'; import type { components } from '@octokit/openapi-types'; type PendingDeployment = components['schemas']['pending-deployment']; +type Commit = components['schemas']['commit']; // // https://octokit.github.io/rest.js/v21/#repos-get-collaborator-permission-level // // https://docs.github.com/en/rest/collaborators/collaborators#list-repository-collaborators @@ -85,6 +86,16 @@ type PendingDeployment = components['schemas']['pending-deployment']; // return commits; // } +// https://octokit.github.io/rest.js/v21/#repos-get-commit +// https://docs.github.com/en/rest/commits/commits#get-a-commit +export async function getCommit(context: any, sha: string): Promise { + const request = context.repo({ + sha, + }); + const { data: commit } = await context.octokit.rest.repos.getCommit(request); + return commit; +} + // https://octokit.github.io/rest.js/v21/#pulls-list-reviews // https://docs.github.com/en/rest/pulls/reviews#list-reviews-for-a-pull-request export async function listPullRequestReviews( diff --git a/src/handlers/deployment-protection-rule.ts b/src/handlers/deployment-protection-rule.ts index a12a001..64b1b79 100644 --- a/src/handlers/deployment-protection-rule.ts +++ b/src/handlers/deployment-protection-rule.ts @@ -25,10 +25,6 @@ export async function handleDeploymentProtectionRule( event, deployment: { id: deployment?.id, - creator: { - id: deployment?.creator.id, - login: deployment?.creator.login, - }, ref: deployment?.ref, sha: deployment?.sha, }, @@ -36,7 +32,7 @@ export async function handleDeploymentProtectionRule( context.log.info( 'Received deployment protection rule event: %s', - JSON.stringify(eventDetails, null, 2), + JSON.stringify(eventDetails), ); if (!deployment || !event || !environment || !callbackUrl) { @@ -52,18 +48,22 @@ export async function handleDeploymentProtectionRule( return; } + // Get the commit that triggered the workflow run + const commit = await GitHubClient.getCommit(context, deployment.sha); + + // Approve deployment if the commit author is in the bypass list const bypassActors = process.env.BYPASS_ACTORS?.split(',') ?? []; - if (bypassActors.includes(deployment.creator.id.toString())) { + if (commit.author?.id && bypassActors.includes(commit.author.id.toString())) { return context.octokit.request(`POST ${callbackUrl}`, { environment_name: environment, state: 'approved', - comment: `Approved via bypass actors list for ${deployment.creator.login}`, + comment: `Approved via bypass actors list for ${commit.author.login}`, }); } context.log.debug( - 'Actor is not included in bypass actors: %s', - deployment.creator.login, + 'Commit author is not included in bypass actors: %s', + commit.author?.login, ); const client = await context.octokit.apps.getAuthenticated(); @@ -75,11 +75,13 @@ export async function handleDeploymentProtectionRule( pull.number, ); + // Find an eligible review authored by a different user than the commit author or committer const deployReview = reviews.find( (review) => ['approved', 'commented'].includes(review.state.toLowerCase()) && review.commit_id === deployment.sha && - review.user.id !== deployment.creator.id && + review.user.id !== commit.author?.id && + review.user.id !== commit.committer?.id && review.body?.startsWith('/deploy'), ); diff --git a/src/handlers/pull-request-review.ts b/src/handlers/pull-request-review.ts index 4fd8603..7f59ce2 100644 --- a/src/handlers/pull-request-review.ts +++ b/src/handlers/pull-request-review.ts @@ -22,7 +22,7 @@ export async function handlePullRequestReview(context: Context) { context.log.info( 'Received pull request review event: %s', - JSON.stringify(eventDetails, null, 2), + JSON.stringify(eventDetails), ); if (!['approved', 'commented'].includes(review.state.toLowerCase())) { @@ -40,44 +40,61 @@ export async function handlePullRequestReview(context: Context) { return; } + // Get the commit of the submitted review + const commit = await GitHubClient.getCommit(context, review.commit_id); + + if (commit.author?.id === review.user.id) { + context.log.debug( + 'Ignoring review by author of the commit: %s', + review.user.login, + ); + return; + } + + if (commit.committer?.id === review.user.id) { + context.log.debug( + 'Ignoring review by committer of the commit: %s', + review.user.login, + ); + return; + } + const workflowRuns = await GitHubClient.listWorkflowRuns( context, review.commit_id, ); await Promise.all( - workflowRuns - .filter((workflowRun) => workflowRun.actor.id !== review.user.id) - .map(async (workflowRun: WorkflowRun) => { - const pendingDeployments = await GitHubClient.listPendingDeployments( - context, + workflowRuns.map(async (workflowRun: WorkflowRun) => { + const pendingDeployments = await GitHubClient.listPendingDeployments( + context, + workflowRun.id, + ); + + if (pendingDeployments.length === 0) { + context.log.info( + 'No pending deployments found for workflow run %s', workflowRun.id, ); + return; + } - if (pendingDeployments.length === 0) { - context.log.info( - 'No pending deployments found for workflow run %s', - workflowRun.id, - ); - return; - } - - const environmentNames = pendingDeployments - .filter((deployment) => deployment.current_user_can_approve) - .filter((deployment) => deployment.environment.name !== undefined) - .map((deployment) => deployment.environment.name!); + const environmentNames = pendingDeployments + .filter((deployment) => deployment.current_user_can_approve) + .filter((deployment) => deployment.environment.name !== undefined) + .map((deployment) => deployment.environment.name!); - await Promise.all( - environmentNames.map((environmentName) => - GitHubClient.reviewWorkflowRun( - context, - workflowRun.id, - environmentName, - 'approved', - `Approved by ${review.user.login} via [review](${review.html_url})`, - ), + await Promise.all( + environmentNames.map((environmentName) => + GitHubClient.reviewWorkflowRun( + context, + workflowRun.id, + environmentName, + 'approved', + `Approved by ${review.user.login} via [review](${review.html_url})`, ), - ); - }), + ), + ); + }), ); } diff --git a/test/fixtures/pull_request_review_comment.created.json b/test/fixtures/pull_request_review_comment.created.json deleted file mode 100644 index 28b324f..0000000 --- a/test/fixtures/pull_request_review_comment.created.json +++ /dev/null @@ -1,600 +0,0 @@ -{ - "action": "created", - "comment": { - "url": "https://api.github.com/repos/balena-io-experimental/deployable/pulls/comments/1856689298", - "pull_request_review_id": 2458558740, - "id": 1856689298, - "node_id": "PRRC_kwDONCUcDs5uqtSS", - "diff_hunk": "@@ -0,0 +1,3 @@\n+const fs = require('fs');\n+\n+module.exports = JSON.parse(fs.readFileSync('./node_modules/@balena/lint/config/.prettierrc', 'utf8'));", - "path": ".prettierrc.js", - "commit_id": "91707f292be6028f598270d2ed6fedeba18670be", - "original_commit_id": "91707f292be6028f598270d2ed6fedeba18670be", - "user": { - "login": "klutchell", - "id": 20458272, - "node_id": "MDQ6VXNlcjIwNDU4Mjcy", - "avatar_url": "https://avatars.githubusercontent.com/u/20458272?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/klutchell", - "html_url": "https://github.com/klutchell", - "followers_url": "https://api.github.com/users/klutchell/followers", - "following_url": "https://api.github.com/users/klutchell/following{/other_user}", - "gists_url": "https://api.github.com/users/klutchell/gists{/gist_id}", - "starred_url": "https://api.github.com/users/klutchell/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/klutchell/subscriptions", - "organizations_url": "https://api.github.com/users/klutchell/orgs", - "repos_url": "https://api.github.com/users/klutchell/repos", - "events_url": "https://api.github.com/users/klutchell/events{/privacy}", - "received_events_url": "https://api.github.com/users/klutchell/received_events", - "type": "User", - "user_view_type": "public", - "site_admin": false - }, - "body": "This isn't working", - "created_at": "2024-11-25T14:14:43Z", - "updated_at": "2024-11-25T14:14:44Z", - "html_url": "https://github.com/balena-io-experimental/deployable/pull/2#discussion_r1856689298", - "pull_request_url": "https://api.github.com/repos/balena-io-experimental/deployable/pulls/2", - "author_association": "COLLABORATOR", - "_links": { - "self": { - "href": "https://api.github.com/repos/balena-io-experimental/deployable/pulls/comments/1856689298" - }, - "html": { - "href": "https://github.com/balena-io-experimental/deployable/pull/2#discussion_r1856689298" - }, - "pull_request": { - "href": "https://api.github.com/repos/balena-io-experimental/deployable/pulls/2" - } - }, - "reactions": { - "url": "https://api.github.com/repos/balena-io-experimental/deployable/pulls/comments/1856689298/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "start_line": null, - "original_start_line": null, - "start_side": null, - "line": 3, - "original_line": 3, - "side": "RIGHT", - "original_position": 3, - "position": 3, - "subject_type": "line" - }, - "pull_request": { - "url": "https://api.github.com/repos/balena-io-experimental/deployable/pulls/2", - "id": 2188445505, - "node_id": "PR_kwDONCUcDs6CcQdB", - "html_url": "https://github.com/balena-io-experimental/deployable/pull/2", - "diff_url": "https://github.com/balena-io-experimental/deployable/pull/2.diff", - "patch_url": "https://github.com/balena-io-experimental/deployable/pull/2.patch", - "issue_url": "https://api.github.com/repos/balena-io-experimental/deployable/issues/2", - "number": 2, - "state": "open", - "locked": false, - "title": "Initial commit of partial app logic", - "user": { - "login": "klutchell", - "id": 20458272, - "node_id": "MDQ6VXNlcjIwNDU4Mjcy", - "avatar_url": "https://avatars.githubusercontent.com/u/20458272?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/klutchell", - "html_url": "https://github.com/klutchell", - "followers_url": "https://api.github.com/users/klutchell/followers", - "following_url": "https://api.github.com/users/klutchell/following{/other_user}", - "gists_url": "https://api.github.com/users/klutchell/gists{/gist_id}", - "starred_url": "https://api.github.com/users/klutchell/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/klutchell/subscriptions", - "organizations_url": "https://api.github.com/users/klutchell/orgs", - "repos_url": "https://api.github.com/users/klutchell/repos", - "events_url": "https://api.github.com/users/klutchell/events{/privacy}", - "received_events_url": "https://api.github.com/users/klutchell/received_events", - "type": "User", - "user_view_type": "public", - "site_admin": false - }, - "body": null, - "created_at": "2024-11-19T20:57:39Z", - "updated_at": "2024-11-25T14:14:44Z", - "closed_at": null, - "merged_at": null, - "merge_commit_sha": "4fcecaf28fc9f124c541486b8c59cb33d695754a", - "assignee": null, - "assignees": [], - "requested_reviewers": [], - "requested_teams": [], - "labels": [], - "milestone": null, - "draft": false, - "commits_url": "https://api.github.com/repos/balena-io-experimental/deployable/pulls/2/commits", - "review_comments_url": "https://api.github.com/repos/balena-io-experimental/deployable/pulls/2/comments", - "review_comment_url": "https://api.github.com/repos/balena-io-experimental/deployable/pulls/comments{/number}", - "comments_url": "https://api.github.com/repos/balena-io-experimental/deployable/issues/2/comments", - "statuses_url": "https://api.github.com/repos/balena-io-experimental/deployable/statuses/91707f292be6028f598270d2ed6fedeba18670be", - "head": { - "label": "balena-io-experimental:kyle/develop", - "ref": "kyle/develop", - "sha": "91707f292be6028f598270d2ed6fedeba18670be", - "user": { - "login": "balena-io-experimental", - "id": 18610741, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjE4NjEwNzQx", - "avatar_url": "https://avatars.githubusercontent.com/u/18610741?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/balena-io-experimental", - "html_url": "https://github.com/balena-io-experimental", - "followers_url": "https://api.github.com/users/balena-io-experimental/followers", - "following_url": "https://api.github.com/users/balena-io-experimental/following{/other_user}", - "gists_url": "https://api.github.com/users/balena-io-experimental/gists{/gist_id}", - "starred_url": "https://api.github.com/users/balena-io-experimental/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/balena-io-experimental/subscriptions", - "organizations_url": "https://api.github.com/users/balena-io-experimental/orgs", - "repos_url": "https://api.github.com/users/balena-io-experimental/repos", - "events_url": "https://api.github.com/users/balena-io-experimental/events{/privacy}", - "received_events_url": "https://api.github.com/users/balena-io-experimental/received_events", - "type": "Organization", - "user_view_type": "public", - "site_admin": false - }, - "repo": { - "id": 874847246, - "node_id": "R_kgDONCUcDg", - "name": "deployable", - "full_name": "balena-io-experimental/deployable", - "private": true, - "owner": { - "login": "balena-io-experimental", - "id": 18610741, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjE4NjEwNzQx", - "avatar_url": "https://avatars.githubusercontent.com/u/18610741?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/balena-io-experimental", - "html_url": "https://github.com/balena-io-experimental", - "followers_url": "https://api.github.com/users/balena-io-experimental/followers", - "following_url": "https://api.github.com/users/balena-io-experimental/following{/other_user}", - "gists_url": "https://api.github.com/users/balena-io-experimental/gists{/gist_id}", - "starred_url": "https://api.github.com/users/balena-io-experimental/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/balena-io-experimental/subscriptions", - "organizations_url": "https://api.github.com/users/balena-io-experimental/orgs", - "repos_url": "https://api.github.com/users/balena-io-experimental/repos", - "events_url": "https://api.github.com/users/balena-io-experimental/events{/privacy}", - "received_events_url": "https://api.github.com/users/balena-io-experimental/received_events", - "type": "Organization", - "user_view_type": "public", - "site_admin": false - }, - "html_url": "https://github.com/balena-io-experimental/deployable", - "description": "Approve deployments with comment reactions", - "fork": false, - "url": "https://api.github.com/repos/balena-io-experimental/deployable", - "forks_url": "https://api.github.com/repos/balena-io-experimental/deployable/forks", - "keys_url": "https://api.github.com/repos/balena-io-experimental/deployable/keys{/key_id}", - "collaborators_url": "https://api.github.com/repos/balena-io-experimental/deployable/collaborators{/collaborator}", - "teams_url": "https://api.github.com/repos/balena-io-experimental/deployable/teams", - "hooks_url": "https://api.github.com/repos/balena-io-experimental/deployable/hooks", - "issue_events_url": "https://api.github.com/repos/balena-io-experimental/deployable/issues/events{/number}", - "events_url": "https://api.github.com/repos/balena-io-experimental/deployable/events", - "assignees_url": "https://api.github.com/repos/balena-io-experimental/deployable/assignees{/user}", - "branches_url": "https://api.github.com/repos/balena-io-experimental/deployable/branches{/branch}", - "tags_url": "https://api.github.com/repos/balena-io-experimental/deployable/tags", - "blobs_url": "https://api.github.com/repos/balena-io-experimental/deployable/git/blobs{/sha}", - "git_tags_url": "https://api.github.com/repos/balena-io-experimental/deployable/git/tags{/sha}", - "git_refs_url": "https://api.github.com/repos/balena-io-experimental/deployable/git/refs{/sha}", - "trees_url": "https://api.github.com/repos/balena-io-experimental/deployable/git/trees{/sha}", - "statuses_url": "https://api.github.com/repos/balena-io-experimental/deployable/statuses/{sha}", - "languages_url": "https://api.github.com/repos/balena-io-experimental/deployable/languages", - "stargazers_url": "https://api.github.com/repos/balena-io-experimental/deployable/stargazers", - "contributors_url": "https://api.github.com/repos/balena-io-experimental/deployable/contributors", - "subscribers_url": "https://api.github.com/repos/balena-io-experimental/deployable/subscribers", - "subscription_url": "https://api.github.com/repos/balena-io-experimental/deployable/subscription", - "commits_url": "https://api.github.com/repos/balena-io-experimental/deployable/commits{/sha}", - "git_commits_url": "https://api.github.com/repos/balena-io-experimental/deployable/git/commits{/sha}", - "comments_url": "https://api.github.com/repos/balena-io-experimental/deployable/comments{/number}", - "issue_comment_url": "https://api.github.com/repos/balena-io-experimental/deployable/issues/comments{/number}", - "contents_url": "https://api.github.com/repos/balena-io-experimental/deployable/contents/{+path}", - "compare_url": "https://api.github.com/repos/balena-io-experimental/deployable/compare/{base}...{head}", - "merges_url": "https://api.github.com/repos/balena-io-experimental/deployable/merges", - "archive_url": "https://api.github.com/repos/balena-io-experimental/deployable/{archive_format}{/ref}", - "downloads_url": "https://api.github.com/repos/balena-io-experimental/deployable/downloads", - "issues_url": "https://api.github.com/repos/balena-io-experimental/deployable/issues{/number}", - "pulls_url": "https://api.github.com/repos/balena-io-experimental/deployable/pulls{/number}", - "milestones_url": "https://api.github.com/repos/balena-io-experimental/deployable/milestones{/number}", - "notifications_url": "https://api.github.com/repos/balena-io-experimental/deployable/notifications{?since,all,participating}", - "labels_url": "https://api.github.com/repos/balena-io-experimental/deployable/labels{/name}", - "releases_url": "https://api.github.com/repos/balena-io-experimental/deployable/releases{/id}", - "deployments_url": "https://api.github.com/repos/balena-io-experimental/deployable/deployments", - "created_at": "2024-10-18T15:11:02Z", - "updated_at": "2024-10-18T15:12:13Z", - "pushed_at": "2024-11-25T00:16:18Z", - "git_url": "git://github.com/balena-io-experimental/deployable.git", - "ssh_url": "git@github.com:balena-io-experimental/deployable.git", - "clone_url": "https://github.com/balena-io-experimental/deployable.git", - "svn_url": "https://github.com/balena-io-experimental/deployable", - "homepage": null, - "size": 171, - "stargazers_count": 0, - "watchers_count": 0, - "language": "TypeScript", - "has_issues": true, - "has_projects": true, - "has_downloads": true, - "has_wiki": true, - "has_pages": false, - "has_discussions": false, - "forks_count": 0, - "mirror_url": null, - "archived": false, - "disabled": false, - "open_issues_count": 2, - "license": { - "key": "isc", - "name": "ISC License", - "spdx_id": "ISC", - "url": "https://api.github.com/licenses/isc", - "node_id": "MDc6TGljZW5zZTEw" - }, - "allow_forking": false, - "is_template": false, - "web_commit_signoff_required": false, - "topics": [], - "visibility": "internal", - "forks": 0, - "open_issues": 2, - "watchers": 0, - "default_branch": "main", - "allow_squash_merge": false, - "allow_merge_commit": true, - "allow_rebase_merge": false, - "allow_auto_merge": true, - "delete_branch_on_merge": true, - "allow_update_branch": false, - "use_squash_pr_title_as_default": false, - "squash_merge_commit_message": "COMMIT_MESSAGES", - "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", - "merge_commit_message": "PR_TITLE", - "merge_commit_title": "MERGE_MESSAGE" - } - }, - "base": { - "label": "balena-io-experimental:main", - "ref": "main", - "sha": "89354cd1344b8de289e5965328831992448eac7a", - "user": { - "login": "balena-io-experimental", - "id": 18610741, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjE4NjEwNzQx", - "avatar_url": "https://avatars.githubusercontent.com/u/18610741?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/balena-io-experimental", - "html_url": "https://github.com/balena-io-experimental", - "followers_url": "https://api.github.com/users/balena-io-experimental/followers", - "following_url": "https://api.github.com/users/balena-io-experimental/following{/other_user}", - "gists_url": "https://api.github.com/users/balena-io-experimental/gists{/gist_id}", - "starred_url": "https://api.github.com/users/balena-io-experimental/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/balena-io-experimental/subscriptions", - "organizations_url": "https://api.github.com/users/balena-io-experimental/orgs", - "repos_url": "https://api.github.com/users/balena-io-experimental/repos", - "events_url": "https://api.github.com/users/balena-io-experimental/events{/privacy}", - "received_events_url": "https://api.github.com/users/balena-io-experimental/received_events", - "type": "Organization", - "user_view_type": "public", - "site_admin": false - }, - "repo": { - "id": 874847246, - "node_id": "R_kgDONCUcDg", - "name": "deployable", - "full_name": "balena-io-experimental/deployable", - "private": true, - "owner": { - "login": "balena-io-experimental", - "id": 18610741, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjE4NjEwNzQx", - "avatar_url": "https://avatars.githubusercontent.com/u/18610741?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/balena-io-experimental", - "html_url": "https://github.com/balena-io-experimental", - "followers_url": "https://api.github.com/users/balena-io-experimental/followers", - "following_url": "https://api.github.com/users/balena-io-experimental/following{/other_user}", - "gists_url": "https://api.github.com/users/balena-io-experimental/gists{/gist_id}", - "starred_url": "https://api.github.com/users/balena-io-experimental/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/balena-io-experimental/subscriptions", - "organizations_url": "https://api.github.com/users/balena-io-experimental/orgs", - "repos_url": "https://api.github.com/users/balena-io-experimental/repos", - "events_url": "https://api.github.com/users/balena-io-experimental/events{/privacy}", - "received_events_url": "https://api.github.com/users/balena-io-experimental/received_events", - "type": "Organization", - "user_view_type": "public", - "site_admin": false - }, - "html_url": "https://github.com/balena-io-experimental/deployable", - "description": "Approve deployments with comment reactions", - "fork": false, - "url": "https://api.github.com/repos/balena-io-experimental/deployable", - "forks_url": "https://api.github.com/repos/balena-io-experimental/deployable/forks", - "keys_url": "https://api.github.com/repos/balena-io-experimental/deployable/keys{/key_id}", - "collaborators_url": "https://api.github.com/repos/balena-io-experimental/deployable/collaborators{/collaborator}", - "teams_url": "https://api.github.com/repos/balena-io-experimental/deployable/teams", - "hooks_url": "https://api.github.com/repos/balena-io-experimental/deployable/hooks", - "issue_events_url": "https://api.github.com/repos/balena-io-experimental/deployable/issues/events{/number}", - "events_url": "https://api.github.com/repos/balena-io-experimental/deployable/events", - "assignees_url": "https://api.github.com/repos/balena-io-experimental/deployable/assignees{/user}", - "branches_url": "https://api.github.com/repos/balena-io-experimental/deployable/branches{/branch}", - "tags_url": "https://api.github.com/repos/balena-io-experimental/deployable/tags", - "blobs_url": "https://api.github.com/repos/balena-io-experimental/deployable/git/blobs{/sha}", - "git_tags_url": "https://api.github.com/repos/balena-io-experimental/deployable/git/tags{/sha}", - "git_refs_url": "https://api.github.com/repos/balena-io-experimental/deployable/git/refs{/sha}", - "trees_url": "https://api.github.com/repos/balena-io-experimental/deployable/git/trees{/sha}", - "statuses_url": "https://api.github.com/repos/balena-io-experimental/deployable/statuses/{sha}", - "languages_url": "https://api.github.com/repos/balena-io-experimental/deployable/languages", - "stargazers_url": "https://api.github.com/repos/balena-io-experimental/deployable/stargazers", - "contributors_url": "https://api.github.com/repos/balena-io-experimental/deployable/contributors", - "subscribers_url": "https://api.github.com/repos/balena-io-experimental/deployable/subscribers", - "subscription_url": "https://api.github.com/repos/balena-io-experimental/deployable/subscription", - "commits_url": "https://api.github.com/repos/balena-io-experimental/deployable/commits{/sha}", - "git_commits_url": "https://api.github.com/repos/balena-io-experimental/deployable/git/commits{/sha}", - "comments_url": "https://api.github.com/repos/balena-io-experimental/deployable/comments{/number}", - "issue_comment_url": "https://api.github.com/repos/balena-io-experimental/deployable/issues/comments{/number}", - "contents_url": "https://api.github.com/repos/balena-io-experimental/deployable/contents/{+path}", - "compare_url": "https://api.github.com/repos/balena-io-experimental/deployable/compare/{base}...{head}", - "merges_url": "https://api.github.com/repos/balena-io-experimental/deployable/merges", - "archive_url": "https://api.github.com/repos/balena-io-experimental/deployable/{archive_format}{/ref}", - "downloads_url": "https://api.github.com/repos/balena-io-experimental/deployable/downloads", - "issues_url": "https://api.github.com/repos/balena-io-experimental/deployable/issues{/number}", - "pulls_url": "https://api.github.com/repos/balena-io-experimental/deployable/pulls{/number}", - "milestones_url": "https://api.github.com/repos/balena-io-experimental/deployable/milestones{/number}", - "notifications_url": "https://api.github.com/repos/balena-io-experimental/deployable/notifications{?since,all,participating}", - "labels_url": "https://api.github.com/repos/balena-io-experimental/deployable/labels{/name}", - "releases_url": "https://api.github.com/repos/balena-io-experimental/deployable/releases{/id}", - "deployments_url": "https://api.github.com/repos/balena-io-experimental/deployable/deployments", - "created_at": "2024-10-18T15:11:02Z", - "updated_at": "2024-10-18T15:12:13Z", - "pushed_at": "2024-11-25T00:16:18Z", - "git_url": "git://github.com/balena-io-experimental/deployable.git", - "ssh_url": "git@github.com:balena-io-experimental/deployable.git", - "clone_url": "https://github.com/balena-io-experimental/deployable.git", - "svn_url": "https://github.com/balena-io-experimental/deployable", - "homepage": null, - "size": 171, - "stargazers_count": 0, - "watchers_count": 0, - "language": "TypeScript", - "has_issues": true, - "has_projects": true, - "has_downloads": true, - "has_wiki": true, - "has_pages": false, - "has_discussions": false, - "forks_count": 0, - "mirror_url": null, - "archived": false, - "disabled": false, - "open_issues_count": 2, - "license": { - "key": "isc", - "name": "ISC License", - "spdx_id": "ISC", - "url": "https://api.github.com/licenses/isc", - "node_id": "MDc6TGljZW5zZTEw" - }, - "allow_forking": false, - "is_template": false, - "web_commit_signoff_required": false, - "topics": [], - "visibility": "internal", - "forks": 0, - "open_issues": 2, - "watchers": 0, - "default_branch": "main", - "allow_squash_merge": false, - "allow_merge_commit": true, - "allow_rebase_merge": false, - "allow_auto_merge": true, - "delete_branch_on_merge": true, - "allow_update_branch": false, - "use_squash_pr_title_as_default": false, - "squash_merge_commit_message": "COMMIT_MESSAGES", - "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", - "merge_commit_message": "PR_TITLE", - "merge_commit_title": "MERGE_MESSAGE" - } - }, - "_links": { - "self": { - "href": "https://api.github.com/repos/balena-io-experimental/deployable/pulls/2" - }, - "html": { - "href": "https://github.com/balena-io-experimental/deployable/pull/2" - }, - "issue": { - "href": "https://api.github.com/repos/balena-io-experimental/deployable/issues/2" - }, - "comments": { - "href": "https://api.github.com/repos/balena-io-experimental/deployable/issues/2/comments" - }, - "review_comments": { - "href": "https://api.github.com/repos/balena-io-experimental/deployable/pulls/2/comments" - }, - "review_comment": { - "href": "https://api.github.com/repos/balena-io-experimental/deployable/pulls/comments{/number}" - }, - "commits": { - "href": "https://api.github.com/repos/balena-io-experimental/deployable/pulls/2/commits" - }, - "statuses": { - "href": "https://api.github.com/repos/balena-io-experimental/deployable/statuses/91707f292be6028f598270d2ed6fedeba18670be" - } - }, - "author_association": "COLLABORATOR", - "auto_merge": null, - "active_lock_reason": null - }, - "repository": { - "id": 874847246, - "node_id": "R_kgDONCUcDg", - "name": "deployable", - "full_name": "balena-io-experimental/deployable", - "private": true, - "owner": { - "login": "balena-io-experimental", - "id": 18610741, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjE4NjEwNzQx", - "avatar_url": "https://avatars.githubusercontent.com/u/18610741?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/balena-io-experimental", - "html_url": "https://github.com/balena-io-experimental", - "followers_url": "https://api.github.com/users/balena-io-experimental/followers", - "following_url": "https://api.github.com/users/balena-io-experimental/following{/other_user}", - "gists_url": "https://api.github.com/users/balena-io-experimental/gists{/gist_id}", - "starred_url": "https://api.github.com/users/balena-io-experimental/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/balena-io-experimental/subscriptions", - "organizations_url": "https://api.github.com/users/balena-io-experimental/orgs", - "repos_url": "https://api.github.com/users/balena-io-experimental/repos", - "events_url": "https://api.github.com/users/balena-io-experimental/events{/privacy}", - "received_events_url": "https://api.github.com/users/balena-io-experimental/received_events", - "type": "Organization", - "user_view_type": "public", - "site_admin": false - }, - "html_url": "https://github.com/balena-io-experimental/deployable", - "description": "Approve deployments with comment reactions", - "fork": false, - "url": "https://api.github.com/repos/balena-io-experimental/deployable", - "forks_url": "https://api.github.com/repos/balena-io-experimental/deployable/forks", - "keys_url": "https://api.github.com/repos/balena-io-experimental/deployable/keys{/key_id}", - "collaborators_url": "https://api.github.com/repos/balena-io-experimental/deployable/collaborators{/collaborator}", - "teams_url": "https://api.github.com/repos/balena-io-experimental/deployable/teams", - "hooks_url": "https://api.github.com/repos/balena-io-experimental/deployable/hooks", - "issue_events_url": "https://api.github.com/repos/balena-io-experimental/deployable/issues/events{/number}", - "events_url": "https://api.github.com/repos/balena-io-experimental/deployable/events", - "assignees_url": "https://api.github.com/repos/balena-io-experimental/deployable/assignees{/user}", - "branches_url": "https://api.github.com/repos/balena-io-experimental/deployable/branches{/branch}", - "tags_url": "https://api.github.com/repos/balena-io-experimental/deployable/tags", - "blobs_url": "https://api.github.com/repos/balena-io-experimental/deployable/git/blobs{/sha}", - "git_tags_url": "https://api.github.com/repos/balena-io-experimental/deployable/git/tags{/sha}", - "git_refs_url": "https://api.github.com/repos/balena-io-experimental/deployable/git/refs{/sha}", - "trees_url": "https://api.github.com/repos/balena-io-experimental/deployable/git/trees{/sha}", - "statuses_url": "https://api.github.com/repos/balena-io-experimental/deployable/statuses/{sha}", - "languages_url": "https://api.github.com/repos/balena-io-experimental/deployable/languages", - "stargazers_url": "https://api.github.com/repos/balena-io-experimental/deployable/stargazers", - "contributors_url": "https://api.github.com/repos/balena-io-experimental/deployable/contributors", - "subscribers_url": "https://api.github.com/repos/balena-io-experimental/deployable/subscribers", - "subscription_url": "https://api.github.com/repos/balena-io-experimental/deployable/subscription", - "commits_url": "https://api.github.com/repos/balena-io-experimental/deployable/commits{/sha}", - "git_commits_url": "https://api.github.com/repos/balena-io-experimental/deployable/git/commits{/sha}", - "comments_url": "https://api.github.com/repos/balena-io-experimental/deployable/comments{/number}", - "issue_comment_url": "https://api.github.com/repos/balena-io-experimental/deployable/issues/comments{/number}", - "contents_url": "https://api.github.com/repos/balena-io-experimental/deployable/contents/{+path}", - "compare_url": "https://api.github.com/repos/balena-io-experimental/deployable/compare/{base}...{head}", - "merges_url": "https://api.github.com/repos/balena-io-experimental/deployable/merges", - "archive_url": "https://api.github.com/repos/balena-io-experimental/deployable/{archive_format}{/ref}", - "downloads_url": "https://api.github.com/repos/balena-io-experimental/deployable/downloads", - "issues_url": "https://api.github.com/repos/balena-io-experimental/deployable/issues{/number}", - "pulls_url": "https://api.github.com/repos/balena-io-experimental/deployable/pulls{/number}", - "milestones_url": "https://api.github.com/repos/balena-io-experimental/deployable/milestones{/number}", - "notifications_url": "https://api.github.com/repos/balena-io-experimental/deployable/notifications{?since,all,participating}", - "labels_url": "https://api.github.com/repos/balena-io-experimental/deployable/labels{/name}", - "releases_url": "https://api.github.com/repos/balena-io-experimental/deployable/releases{/id}", - "deployments_url": "https://api.github.com/repos/balena-io-experimental/deployable/deployments", - "created_at": "2024-10-18T15:11:02Z", - "updated_at": "2024-10-18T15:12:13Z", - "pushed_at": "2024-11-25T00:16:18Z", - "git_url": "git://github.com/balena-io-experimental/deployable.git", - "ssh_url": "git@github.com:balena-io-experimental/deployable.git", - "clone_url": "https://github.com/balena-io-experimental/deployable.git", - "svn_url": "https://github.com/balena-io-experimental/deployable", - "homepage": null, - "size": 171, - "stargazers_count": 0, - "watchers_count": 0, - "language": "TypeScript", - "has_issues": true, - "has_projects": true, - "has_downloads": true, - "has_wiki": true, - "has_pages": false, - "has_discussions": false, - "forks_count": 0, - "mirror_url": null, - "archived": false, - "disabled": false, - "open_issues_count": 2, - "license": { - "key": "isc", - "name": "ISC License", - "spdx_id": "ISC", - "url": "https://api.github.com/licenses/isc", - "node_id": "MDc6TGljZW5zZTEw" - }, - "allow_forking": false, - "is_template": false, - "web_commit_signoff_required": false, - "topics": [], - "visibility": "internal", - "forks": 0, - "open_issues": 2, - "watchers": 0, - "default_branch": "main", - "custom_properties": {} - }, - "organization": { - "login": "balena-io-experimental", - "id": 18610741, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjE4NjEwNzQx", - "url": "https://api.github.com/orgs/balena-io-experimental", - "repos_url": "https://api.github.com/orgs/balena-io-experimental/repos", - "events_url": "https://api.github.com/orgs/balena-io-experimental/events", - "hooks_url": "https://api.github.com/orgs/balena-io-experimental/hooks", - "issues_url": "https://api.github.com/orgs/balena-io-experimental/issues", - "members_url": "https://api.github.com/orgs/balena-io-experimental/members{/member}", - "public_members_url": "https://api.github.com/orgs/balena-io-experimental/public_members{/member}", - "avatar_url": "https://avatars.githubusercontent.com/u/18610741?v=4", - "description": "A collection of play around and experimental projects for balena IoT devices" - }, - "enterprise": { - "id": 16136, - "slug": "balena", - "name": " Balena Ltd", - "node_id": "E_kgDNPwg", - "avatar_url": "https://avatars.githubusercontent.com/b/16136?v=4", - "description": "Balena brings the benefits of Linux containers to the IoT. Develop iteratively, deploy safely, and manage at scale.", - "website_url": "https://www.balena.io", - "html_url": "https://github.com/enterprises/balena", - "created_at": "2022-10-05T14:08:07Z", - "updated_at": "2024-10-04T08:57:35Z" - }, - "sender": { - "login": "klutchell", - "id": 20458272, - "node_id": "MDQ6VXNlcjIwNDU4Mjcy", - "avatar_url": "https://avatars.githubusercontent.com/u/20458272?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/klutchell", - "html_url": "https://github.com/klutchell", - "followers_url": "https://api.github.com/users/klutchell/followers", - "following_url": "https://api.github.com/users/klutchell/following{/other_user}", - "gists_url": "https://api.github.com/users/klutchell/gists{/gist_id}", - "starred_url": "https://api.github.com/users/klutchell/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/klutchell/subscriptions", - "organizations_url": "https://api.github.com/users/klutchell/orgs", - "repos_url": "https://api.github.com/users/klutchell/repos", - "events_url": "https://api.github.com/users/klutchell/events{/privacy}", - "received_events_url": "https://api.github.com/users/klutchell/received_events", - "type": "User", - "user_view_type": "public", - "site_admin": false - }, - "installation": { - "id": 57364329, - "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uNTczNjQzMjk=" - } -} diff --git a/test/handlers/deployment-protection-rule.test.ts b/test/handlers/deployment-protection-rule.test.ts index c77ac1a..1c0bc6d 100644 --- a/test/handlers/deployment-protection-rule.test.ts +++ b/test/handlers/deployment-protection-rule.test.ts @@ -20,10 +20,6 @@ const testFixtures = { deployment_callback_url: 'https://api.github.com/repos/test-org/test-repo/actions/runs/1234/deployment_protection_rule', deployment: { - creator: { - login: 'bypass-actor', - id: 5, - }, sha: 'test-sha', }, installation: { id: 12345678 }, @@ -60,10 +56,13 @@ describe('Deployment Protection Rule Handler', () => { nock.enableNetConnect(); }); - test('approves deployment created by bypass user', async () => { + test('approves deployment authored by bypass user', async () => { const mock = nock('https://api.github.com') .post('/app/installations/12345678/access_tokens') .reply(200, { token: 'test', permissions: { issues: 'write' } }) + .get('/repos/test-org/test-repo/commits') + .query(true) + .reply(200, { author: { id: 5 }, committer: { id: 123 } }) .post( '/repos/test-org/test-repo/actions/runs/1234/deployment_protection_rule', ) @@ -115,6 +114,9 @@ describe('Deployment Protection Rule Handler', () => { .reply(200, { token: 'test', permissions: { issues: 'write' } }) .get('/app') .reply(200, { id: 456 }) + .get('/repos/test-org/test-repo/commits') + .query(true) + .reply(200, { author: { id: 123 }, committer: { id: 123 } }) .get('/repos/test-org/test-repo/pulls/123/reviews') .reply(200, []) .get('/repos/test-org/test-repo/issues/123/comments') @@ -140,6 +142,9 @@ describe('Deployment Protection Rule Handler', () => { .reply(200, { token: 'test', permissions: { issues: 'write' } }) .get('/app') .reply(200, { id: 456 }) + .get('/repos/test-org/test-repo/commits') + .query(true) + .reply(200, { author: { id: 123 }, committer: { id: 123 } }) .get('/repos/test-org/test-repo/pulls/123/reviews') .reply(200, []) .get('/repos/test-org/test-repo/issues/123/comments') @@ -163,6 +168,9 @@ describe('Deployment Protection Rule Handler', () => { const mock = nock('https://api.github.com') .post('/app/installations/12345678/access_tokens') .reply(200, { token: 'test', permissions: { issues: 'write' } }) + .get('/repos/test-org/test-repo/commits') + .query(true) + .reply(200, { author: { id: 5 }, committer: { id: 123 } }) .post( '/repos/test-org/test-repo/actions/runs/1234/deployment_protection_rule', ) @@ -182,6 +190,9 @@ describe('Deployment Protection Rule Handler', () => { .reply(200, { token: 'test', permissions: { issues: 'write' } }) .get('/app') .reply(200, { id: 456 }) + .get('/repos/test-org/test-repo/commits') + .query(true) + .reply(200, { author: { id: 123 }, committer: { id: 123 } }) .get('/repos/test-org/test-repo/pulls/123/reviews') .reply(200, [ { @@ -212,6 +223,9 @@ describe('Deployment Protection Rule Handler', () => { .reply(200, { token: 'test', permissions: { issues: 'write' } }) .get('/app') .reply(200, { id: 456 }) + .get('/repos/test-org/test-repo/commits') + .query(true) + .reply(200, { author: { id: 123 }, committer: { id: 123 } }) .get('/repos/test-org/test-repo/pulls/123/reviews') .reply(200, [ { @@ -236,19 +250,57 @@ describe('Deployment Protection Rule Handler', () => { expect(mock.pendingMocks()).toStrictEqual([]); }); - test('ignores reviews by deployment creator', async () => { + test('ignores reviews by commit author', async () => { + const mock = nock('https://api.github.com') + .post('/app/installations/12345678/access_tokens') + .reply(200, { token: 'test', permissions: { issues: 'write' } }) + .get('/app') + .reply(200, { id: 456 }) + .get('/repos/test-org/test-repo/commits') + .query(true) + .reply(200, { author: { id: 123 }, committer: { id: 456 } }) + .get('/repos/test-org/test-repo/pulls/123/reviews') + .reply(200, [ + { + commit_id: 'test-sha', + body: '/deploy please', + state: 'COMMENTED', + user: { login: 'test-user', id: 123 }, + }, + ]) + .get('/repos/test-org/test-repo/issues/123/comments') + .reply(200, []) + .post('/repos/test-org/test-repo/issues/123/comments', { + body: instructionalComment, + }) + .reply(200); + + process.env.BYPASS_ACTORS = ''; + + await probot.receive({ + name: 'deployment_protection_rule', + payload: testFixtures.deployment_protection_rule, + }); + + expect(mock.pendingMocks()).toStrictEqual([]); + }); + + test('ignores reviews by commit committer', async () => { const mock = nock('https://api.github.com') .post('/app/installations/12345678/access_tokens') .reply(200, { token: 'test', permissions: { issues: 'write' } }) .get('/app') .reply(200, { id: 456 }) + .get('/repos/test-org/test-repo/commits') + .query(true) + .reply(200, { author: { id: 123 }, committer: { id: 456 } }) .get('/repos/test-org/test-repo/pulls/123/reviews') .reply(200, [ { commit_id: 'test-sha', body: '/deploy please', state: 'COMMENTED', - user: { login: 'test-user', id: 5 }, + user: { login: 'test-user', id: 456 }, }, ]) .get('/repos/test-org/test-repo/issues/123/comments') @@ -274,6 +326,9 @@ describe('Deployment Protection Rule Handler', () => { .reply(200, { token: 'test', permissions: { issues: 'write' } }) .get('/app') .reply(200, { id: 456 }) + .get('/repos/test-org/test-repo/commits') + .query(true) + .reply(200, { author: { id: 123 }, committer: { id: 123 } }) .get('/repos/test-org/test-repo/pulls/123/reviews') .reply(200, [ { @@ -306,6 +361,9 @@ describe('Deployment Protection Rule Handler', () => { .reply(200, { token: 'test', permissions: { issues: 'write' } }) .get('/app') .reply(200, { id: 456 }) + .get('/repos/test-org/test-repo/commits') + .query(true) + .reply(200, { author: { id: 123 }, committer: { id: 123 } }) .get('/repos/test-org/test-repo/pulls/123/reviews') .reply(200, []) .get('/repos/test-org/test-repo/issues/123/comments') diff --git a/test/handlers/pull-request-review.test.ts b/test/handlers/pull-request-review.test.ts index 1b91c5f..5d4b8f6 100644 --- a/test/handlers/pull-request-review.test.ts +++ b/test/handlers/pull-request-review.test.ts @@ -60,6 +60,9 @@ describe('Pull Request Review Handler', () => { const mock = nock('https://api.github.com') .post('/app/installations/12345678/access_tokens') .reply(200, { token: 'test', permissions: { issues: 'write' } }) + .get('/repos/test-org/test-repo/commits') + .query(true) + .reply(200, { author: { id: 123 }, committer: { id: 123 } }) .get('/repos/test-org/test-repo/actions/runs') .query(true) .reply(200, { workflow_runs: [{ id: 1234, actor: { id: 123 } }] }) @@ -135,7 +138,7 @@ describe('Pull Request Review Handler', () => { expect(nock.pendingMocks()).toStrictEqual([]); }); - test('ignores review by workflow run actor', async () => { + test('ignores review by commit author', async () => { const payload = { ...testFixtures.pull_request_review, review: { @@ -150,9 +153,36 @@ describe('Pull Request Review Handler', () => { const mock = nock('https://api.github.com') .post('/app/installations/12345678/access_tokens') .reply(200, { token: 'test', permissions: { issues: 'write' } }) - .get('/repos/test-org/test-repo/actions/runs') + .get('/repos/test-org/test-repo/commits') .query(true) - .reply(200, { workflow_runs: [{ id: 1234, actor: { id: 123 } }] }); + .reply(200, { author: { id: 123 }, committer: { id: 456 } }); + + await probot.receive({ + name: 'pull_request_review', + payload, + }); + + expect(mock.pendingMocks()).toStrictEqual([]); + }); + + test('ignores review by commit committer', async () => { + const payload = { + ...testFixtures.pull_request_review, + review: { + ...testFixtures.pull_request_review.review, + user: { + ...testFixtures.pull_request_review.review.user, + id: 123, + }, + }, + }; + + const mock = nock('https://api.github.com') + .post('/app/installations/12345678/access_tokens') + .reply(200, { token: 'test', permissions: { issues: 'write' } }) + .get('/repos/test-org/test-repo/commits') + .query(true) + .reply(200, { author: { id: 456 }, committer: { id: 123 } }); await probot.receive({ name: 'pull_request_review', @@ -166,6 +196,9 @@ describe('Pull Request Review Handler', () => { const mock = nock('https://api.github.com') .post('/app/installations/12345678/access_tokens') .reply(200, { token: 'test', permissions: { issues: 'write' } }) + .get('/repos/test-org/test-repo/commits') + .query(true) + .reply(200, { author: { id: 123 }, committer: { id: 123 } }) .get('/repos/test-org/test-repo/actions/runs') .query(true) .reply(200, { workflow_runs: [] }); @@ -182,6 +215,9 @@ describe('Pull Request Review Handler', () => { const mock = nock('https://api.github.com') .post('/app/installations/12345678/access_tokens') .reply(200, { token: 'test', permissions: { issues: 'write' } }) + .get('/repos/test-org/test-repo/commits') + .query(true) + .reply(200, { author: { id: 123 }, committer: { id: 123 } }) .get('/repos/test-org/test-repo/actions/runs') .query(true) .reply(200, { workflow_runs: [{ id: 1234, actor: { id: 123 } }] }) @@ -200,6 +236,9 @@ describe('Pull Request Review Handler', () => { const mock = nock('https://api.github.com') .post('/app/installations/12345678/access_tokens') .reply(200, { token: 'test', permissions: { issues: 'write' } }) + .get('/repos/test-org/test-repo/commits') + .query(true) + .reply(200, { author: { id: 123 }, committer: { id: 123 } }) .get('/repos/test-org/test-repo/actions/runs') .query(true) .reply(200, { workflow_runs: [{ id: 1234, actor: { id: 123 } }] })