Skip to content

Commit

Permalink
limit-pull-requests: migrate to JS and add test
Browse files Browse the repository at this point in the history
As part of #549.
  • Loading branch information
ZhongRuoyu committed Sep 21, 2024
1 parent f3fe7e5 commit 65e77dd
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 62 deletions.
64 changes: 2 additions & 62 deletions limit-pull-requests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,65 +39,5 @@ inputs:
default: "false"

runs:
using: composite
steps:
- name: Check the number of pull requests
id: count-pull-requests
run: |
# If the user is exempted, assume they have no pull requests.
if grep -Fiqx '${{ github.actor }}' <<<"$EXCEPT_USERS"; then
echo "::notice::@${{ github.actor }} is exempted from the limit."
echo "count=0" >>"$GITHUB_OUTPUT"
exit 0
fi
if grep -Fiqx '${{ github.event.pull_request.author_association }}' <<<"$EXCEPT_AUTHOR_ASSOCIATIONS"; then
echo "::notice::@{{ github.actor }} is a ${{ github.event.pull_request.author_association }} exempted from the limit."
echo "count=0" >>"$GITHUB_OUTPUT"
exit 0
fi
count="$(
gh api \
--method GET \
--header 'Accept: application/vnd.github+json' \
--header 'X-GitHub-Api-Version: 2022-11-28' \
--field state=open \
--paginate \
'/repos/{owner}/{repo}/pulls' |
jq \
--raw-output \
--arg USER '${{ github.actor }}' \
'map(select(.user.login == $USER)) | length'
)"
echo "::notice::@${{ github.actor }} has $count open pull request(s)."
echo "count=$count" >>"$GITHUB_OUTPUT"
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ inputs.token }}
EXCEPT_USERS: ${{ inputs.except-users }}
EXCEPT_AUTHOR_ASSOCIATIONS: ${{ inputs.except-author-associations }}
shell: bash

- name: Comment on pull request
if: >
fromJSON(steps.count-pull-requests.outputs.count) > fromJSON(inputs.comment-limit) &&
inputs.comment != ''
run: |
gh pr comment '${{ github.event.pull_request.number }}' \
--body="${COMMENT_BODY}"
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ inputs.token }}
COMMENT_BODY: ${{ inputs.comment }}
shell: bash

- name: Close pull request
if: >
fromJSON(steps.count-pull-requests.outputs.count) > fromJSON(inputs.close-limit) &&
inputs.close == 'true'
run: |
gh pr close '${{ github.event.pull_request.number }}'
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ inputs.token }}
shell: bash
using: node20
main: main.mjs
74 changes: 74 additions & 0 deletions limit-pull-requests/main.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import core from "@actions/core";
import github from "@actions/github";

async function main() {
try {
const eventName = github.context.eventName;
if (!["pull_request", "pull_request_target"].includes(eventName)) {
core.setFailed(`${eventName} is not a supported event. Only pull_request `
+ `and pull_request_target are supported.`);
return;
}

Check warning on line 11 in limit-pull-requests/main.mjs

View check run for this annotation

Codecov / codecov/patch

limit-pull-requests/main.mjs#L8-L11

Added lines #L8 - L11 were not covered by tests

const token = core.getInput("token", { required: true });

const exceptUsersInput = core.getInput("except-users");
const exceptUsers = exceptUsersInput ? exceptUsersInput.split(",") : [];
const exceptAuthorAssocsInput =
core.getInput("except-author-associations");
const exceptAuthorAssocs =
exceptAuthorAssocsInput ? exceptAuthorAssocsInput.split(",") : [];

const commentLimit = core.getInput("comment-limit");
const comment = core.getInput("comment");
const closeLimit = core.getInput("close-limit");
const close = core.getBooleanInput("close");

if (!comment && !close) {
core.info("No action specified; exiting.");
return;
}

Check warning on line 30 in limit-pull-requests/main.mjs

View check run for this annotation

Codecov / codecov/patch

limit-pull-requests/main.mjs#L28-L30

Added lines #L28 - L30 were not covered by tests
if (exceptUsers.includes(github.context.actor)) {
core.info(`@${github.context.actor} is exempted from the limit.`);
return;
}
if (exceptAuthorAssocs.includes(
github.context.payload.pull_request.author_association)) {
core.info(`@${github.context.actor} is exempted from the limit.`);
return;
}

Check warning on line 39 in limit-pull-requests/main.mjs

View check run for this annotation

Codecov / codecov/patch

limit-pull-requests/main.mjs#L37-L39

Added lines #L37 - L39 were not covered by tests

const client = github.getOctokit(token);
const openPrs = await client.paginate(client.rest.pulls.list, {
...github.context.repo,
state: "open",
per_page: 100,
});
const prCount = openPrs
.filter(pr => pr.user.login === github.context.actor)
.length;
core.info(`@${github.context.actor} has ${prCount} open PR(s).`);

if (comment && prCount >= commentLimit) {
await client.rest.issues.createComment({
...github.context.repo,
issue_number: github.context.payload.pull_request.number,
body: comment,
});
core.notice(`Soft limit reached; commented on PR.`);
}

if (close && prCount >= closeLimit) {
await client.rest.pulls.update({
...github.context.repo,
pull_number: github.context.payload.pull_request.number,
state: "closed",
});
core.notice(`Hard limit reached; closed PR.`);
}
} catch (error) {
core.setFailed(error);
}

Check warning on line 71 in limit-pull-requests/main.mjs

View check run for this annotation

Codecov / codecov/patch

limit-pull-requests/main.mjs#L70-L71

Added lines #L70 - L71 were not covered by tests
}

await main();
130 changes: 130 additions & 0 deletions limit-pull-requests/main.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import util from "node:util";

describe("limit-pull-requests", async () => {
const token = "fake-token";
const commentLimit = 3;
const comment = "Please don't open too many PRs!";
const closeLimit = 5;
const close = true;
const exemptedUser = "exempted-user";

const prNumber = 12345;

const GITHUB_ACTOR = "fake-actor";
const GITHUB_EVENT_NAME = "pull_request_target";

let directory;
let eventPath;

before(async () => {
process.env.GITHUB_ACTOR = GITHUB_ACTOR;
process.env.GITHUB_EVENT_NAME = GITHUB_EVENT_NAME;

directory = await fs.promises.mkdtemp(path.join(os.tmpdir(), "limit-pull-requests-"));
eventPath = path.join(directory, "event.json");
await fs.promises.writeFile(eventPath, JSON.stringify({
pull_request: {
number: prNumber,
author_association: "CONTRIBUTOR",
},
}));

process.env.GITHUB_EVENT_PATH = eventPath;
});

after(async () => {
await fs.promises.rm(directory, { recursive: true });
});

beforeEach(async () => {
mockInput("token", token);
mockInput("comment-limit", commentLimit.toString());
mockInput("comment", comment);
mockInput("close-limit", closeLimit.toString());
mockInput("close", close.toString());
mockInput("except-users", exemptedUser);
});

it("does nothing if no limit is reached", async () => {
const actor = GITHUB_ACTOR;
const mockPool = githubMockPool();

mockPool.intercept({
method: "GET",
path: `/repos/${GITHUB_REPOSITORY}/pulls?per_page=100&state=open`,
headers: {
Authorization: `token ${token}`,
},
}).defaultReplyHeaders({
"Content-Type": "application/json",
}).reply(200, [
{ user: { login: actor } },
{ user: { login: "some-other-actor-1" } },
{ user: { login: "some-other-actor-2" } },
{ user: { login: "some-other-actor-3" } },
]);

await loadMain();
});

it("posts a comment and closes the PR if limits are reached", async () => {
const actor = GITHUB_ACTOR;
const mockPool = githubMockPool();

mockPool.intercept({
method: "GET",
path: `/repos/${GITHUB_REPOSITORY}/pulls?per_page=100&state=open`,
headers: {
Authorization: `token ${token}`,
},
}).defaultReplyHeaders({
"Content-Type": "application/json",
}).reply(200, [
{ user: { login: actor } },
{ user: { login: actor } },
{ user: { login: actor } },
{ user: { login: actor } },
{ user: { login: actor } },
{ user: { login: actor } },
{ user: { login: "some-other-actor-1" } },
{ user: { login: "some-other-actor-2" } },
{ user: { login: "some-other-actor-3" } },
]);

mockPool.intercept({
method: "POST",
path: `/repos/${GITHUB_REPOSITORY}/issues/${prNumber}/comments`,
headers: {
Authorization: `token ${token}`,
},
body: (body) => util.isDeepStrictEqual(JSON.parse(body), {
body: comment,
}),
}).defaultReplyHeaders({
"Content-Type": "application/json",
}).reply(200, {});

mockPool.intercept({
method: "PATCH",
path: `/repos/${GITHUB_REPOSITORY}/pulls/${prNumber}`,
headers: {
Authorization: `token ${token}`,
},
body: (body) => util.isDeepStrictEqual(JSON.parse(body), {
state: "closed",
}),
}).defaultReplyHeaders({
"Content-Type": "application/json",
}).reply(200, {});

await loadMain();
});

it("does nothing if limits are reached but user is exempted", async () => {
process.env.GITHUB_ACTOR = exemptedUser;
await loadMain();
});
});

0 comments on commit 65e77dd

Please sign in to comment.