Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

limit-pull-requests: migrate to JS and add test #589

Merged
merged 1 commit into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
});
});