Skip to content
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
65 changes: 6 additions & 59 deletions README.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ inputs:
description: "Percentile value for timeline"
required: false
default: "75"
REQUIRED_APPROVALS:
description: "Amount of approvals required for PR to be approved"
required: true
default: "1"
TOP_LIST_AMOUNT:
description: "Amount of items in lists"
required: false
Expand Down
14 changes: 10 additions & 4 deletions build/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pull-request-analytics-action",
"version": "4.7.0",
"version": "4.8.0",
"description": "Generates detailed PR analytics reports within GitHub, focusing on review efficiency and team performance.",
"main": "build/index.js",
"scripts": {
Expand Down
3 changes: 3 additions & 0 deletions src/analytics/sendActionRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ export const sendActionRun = () => {
AGGREGATE_VALUE_METHODS: getMultipleValuesInput(
"AGGREGATE_VALUE_METHODS"
),
INCLUDE_USERS: getMultipleValuesInput("INCLUDE_USERS").length,
EXCLUDE_USERS: getMultipleValuesInput("EXCLUDE_USERS").length,
HIDE_USERS: getMultipleValuesInput("HIDE_USERS").length,
SHOW_USERS: getMultipleValuesInput("SHOW_USERS").length,
REQUIRED_APPROVALS: getValueAsIs("REQUIRED_APPROVALS"),
INCLUDE_LABELS: getMultipleValuesInput("INCLUDE_LABELS").length,
EXCLUDE_LABELS: getMultipleValuesInput("EXCLUDE_LABELS").length,
EXECUTION_OUTCOME: getMultipleValuesInput("EXECUTION_OUTCOME"),
Expand Down
49 changes: 39 additions & 10 deletions src/converters/utils/calculations/getApproveTime.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@ const approvedReview = [
},
];

const approvedTwiceReviewWithRequiredApprovals = [
{
state: "approved",
submitted_at: "2024-01-11T07:00:00Z",
user: { login: "dev1" },
},
{
state: "changes_requested",
submitted_at: "2024-01-11T09:00:00Z",
user: { login: "dev2" },
},
{
state: "approved",
submitted_at: "2024-01-11T11:00:00Z",
user: { login: "dev2" },
},
]

const approvedTwiceReview = [
{
state: "approved",
Expand Down Expand Up @@ -132,44 +150,55 @@ const dismissedBySecondReviewer = [

describe("check getApproveTime", () => {
it("Check PR without reviews and return null", () => {
expect(getApproveTime(notReviewed)).toBe(null);
expect(getApproveTime(notReviewed, 1)).toBe(null);
});
it("Check PR with only 1 approval", () => {
expect(getApproveTime(approvedReview)).toBe("2024-01-11T07:00:00Z");
expect(getApproveTime(approvedReview, 1)).toBe("2024-01-11T07:00:00Z");
});
it("Check PR with 2 approval and return the earliest one", () => {
expect(getApproveTime(approvedTwiceReview)).toBe("2024-01-11T07:00:00Z");
expect(getApproveTime(approvedTwiceReview, 1)).toBe("2024-01-11T07:00:00Z");
});
// changes requested included
it("Check PR with approval after changes requested and return time of approval", () => {
expect(getApproveTime(approvedAfterChangesRequestedReview)).toBe(
expect(getApproveTime(approvedAfterChangesRequestedReview, 1)).toBe(
"2024-01-11T09:00:00Z"
);
});
it("Check PR with changes requested by second developer and approval after it. Should return the last time of approval", () => {
expect(getApproveTime(oneApprovedOneChangesRequestedReview)).toBe(
expect(getApproveTime(oneApprovedOneChangesRequestedReview, 1)).toBe(
"2024-01-12T05:00:00Z"
);
});
it("Check PR with approval -> changes requested -> approval and return time of the last approve", () => {
expect(getApproveTime(approvedRequestedChangesApprovedReview)).toBe(
expect(getApproveTime(approvedRequestedChangesApprovedReview, 1)).toBe(
"2024-01-12T09:00:00Z"
);
});
it("Check PR with dismissed changes requested and return time of the approval", () => {
expect(getApproveTime(dismissedChangesRequestedReview)).toBe(
expect(getApproveTime(dismissedChangesRequestedReview, 1)).toBe(
"2024-01-11T09:00:00Z"
);
});
it("Check PR with changes requested and return null", () => {
expect(getApproveTime(changesRequestedReview)).toBe(null);
expect(getApproveTime(changesRequestedReview, 1)).toBe(null);
});
it("Check commented PR and return time of the approval", () => {
expect(getApproveTime(commentedReview)).toBe("2024-01-12T05:00:00Z");
expect(getApproveTime(commentedReview, 1)).toBe("2024-01-12T05:00:00Z");
});
it("Check PR with dismissed changes requested status from second reviewer and return time of the approval", () => {
expect(getApproveTime(dismissedBySecondReviewer)).toBe(
expect(getApproveTime(dismissedBySecondReviewer, 1)).toBe(
"2024-01-11T13:00:00Z"
);
});
it("Check PR with 2 approvals and return time of the second approval", () => {
expect(getApproveTime(approvedTwiceReview, 2)).toBe("2024-01-11T09:00:00Z");
});
it("Check PR with 3 approvals and return time of the third approval", () => {
expect(getApproveTime(approvedTwiceReview, 3)).toBe(null);
});
it("Check PR with 2 approvals and return time of the second approval", () => {
expect(getApproveTime(approvedTwiceReviewWithRequiredApprovals, 2)).toBe(
"2024-01-11T11:00:00Z"
);
});
});
12 changes: 8 additions & 4 deletions src/converters/utils/calculations/getApproveTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { invalidUserLogin } from "../../constants";
import { checkUserInclusive } from "./checkUserInclusive";

export const getApproveTime = (
reviews: Awaited<ReturnType<typeof makeComplexRequest>>["events"][number]
reviews: Awaited<ReturnType<typeof makeComplexRequest>>["events"][number],
requiredApprovals: number
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation for the requiredApprovals parameter.

Consider adding validation to ensure requiredApprovals is a positive integer to prevent unexpected behavior.

export const getApproveTime = (
  reviews: Awaited<ReturnType<typeof makeComplexRequest>>["events"][number],
  requiredApprovals: number
) => {
+  if (requiredApprovals <= 0 || !Number.isInteger(requiredApprovals)) {
+    throw new Error("requiredApprovals must be a positive integer");
+  }
+  
  const statuses = Object.values(
🤖 Prompt for AI Agents
In src/converters/utils/calculations/getApproveTime.ts at line 8, the
requiredApprovals parameter lacks validation. Add a check to ensure
requiredApprovals is a positive integer before using it, throwing an error or
handling invalid values appropriately to prevent unexpected behavior.

) => {
const statuses = Object.values(
reviews?.reduce(
Expand All @@ -13,12 +14,13 @@ export const getApproveTime = (
review: any
) => {
const user = review.user?.login || invalidUserLogin;
if(!checkUserInclusive(user)){
if (!checkUserInclusive(user)) {
return acc;
}
const statusesEntries = Object.keys(acc) as string[];
const isApproved =
statusesEntries.some((user) => acc[user].state === "approved") &&
statusesEntries.filter((user) => acc[user].state === "approved")
.length >= requiredApprovals &&
!statusesEntries.some(
(user) => acc[user].state === "changes_requested"
) &&
Expand All @@ -41,8 +43,10 @@ export const getApproveTime = (
) || {}
);


const isApproved =
statuses.some((status) => status.state === "approved") &&
statuses.filter((status) => status.state === "approved").length >=
requiredApprovals &&
!statuses.some((status) => status.state === "changes_requested");

return isApproved
Expand Down
5 changes: 4 additions & 1 deletion src/converters/utils/preparePullRequestTimeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ export const preparePullRequestTimeline = (
review.user?.login !== pullRequestInfo?.user?.login &&
checkUserInclusive(review.user?.login || invalidUserLogin)
);
const approveTime = getApproveTime(pullRequestReviews);
const approveTime = getApproveTime(
pullRequestReviews,
parseInt(getValueAsIs("REQUIRED_APPROVALS"))
);
Comment on lines +31 to +34
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix parseInt usage and add error handling for the config value.

The logic correctly passes the required approvals parameter, but there are two improvements needed:

  1. Static analysis correctly identifies that Number.parseInt should be used
  2. Add error handling for invalid or missing config values

Apply this diff:

  const approveTime = getApproveTime(
    pullRequestReviews,
-   parseInt(getValueAsIs("REQUIRED_APPROVALS"))
+   Number.parseInt(getValueAsIs("REQUIRED_APPROVALS") || "1", 10)
  );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const approveTime = getApproveTime(
pullRequestReviews,
parseInt(getValueAsIs("REQUIRED_APPROVALS"))
);
const approveTime = getApproveTime(
pullRequestReviews,
Number.parseInt(getValueAsIs("REQUIRED_APPROVALS") || "1", 10)
);
🧰 Tools
🪛 Biome (1.9.4)

[error] 33-33: Use Number.parseInt instead of the equivalent global.

ES2015 moved some globals into the Number namespace for consistency.
Safe fix: Use Number.parseInt instead.

(lint/style/useNumberNamespace)

🤖 Prompt for AI Agents
In src/converters/utils/preparePullRequestTimeline.ts around lines 31 to 34,
replace the use of parseInt with Number.parseInt for better clarity and static
analysis compliance. Additionally, add error handling to check if the config
value retrieved by getValueAsIs("REQUIRED_APPROVALS") is valid and a proper
number before parsing; if invalid or missing, handle the error gracefully, such
as by throwing an informative error or using a default value.


const timeToReviewRequest = calcDifferenceInMinutes(
pullRequestInfo?.created_at,
Expand Down
1 change: 1 addition & 0 deletions src/view/utils/createConfigParamsCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ ${[
"AGGREGATE_VALUE_METHODS",
"SHOW_CORRELATION_GRAPHS",
"SHOW_ACTIVITY_TIME_GRAPHS",
"REQUIRED_APPROVALS",
"PERCENTILE",
"HIDE_USERS",
"SHOW_USERS",
Expand Down