From 029c5e60a99ac5f22e22c35b6a6ef4bf28379817 Mon Sep 17 00:00:00 2001 From: Aleksei Simatov Date: Sun, 30 Nov 2025 17:07:51 +0700 Subject: [PATCH 1/3] Feature: Filtering by branch name --- README.md | 5 + action.yml | 12 + build/index.js | 81 +++++- package-lock.json | 4 +- package.json | 2 +- src/requests/makeComplexRequest.ts | 26 +- src/requests/utils/filterPRs.mock.ts | 70 +++++ src/requests/utils/filterPRs.spec.ts | 328 +++++++++++++++++++++++ src/requests/utils/filterPRs.ts | 79 ++++++ src/requests/utils/index.ts | 1 + src/view/utils/createConfigParamsCode.ts | 4 + 11 files changed, 579 insertions(+), 33 deletions(-) create mode 100644 src/requests/utils/filterPRs.mock.ts create mode 100644 src/requests/utils/filterPRs.spec.ts create mode 100644 src/requests/utils/filterPRs.ts diff --git a/README.md b/README.md index eb3c1a6..557a338 100644 --- a/README.md +++ b/README.md @@ -282,6 +282,10 @@ Below is a table outlining the various configuration parameters available for ** | `INCLUDE_LABELS` | Only PRs with mentioned labels will be included in the report. Values should be separated by commas. Example: `bugfix, enhancement` | - | | `INCLUDE_USERS` | Only data for the specified users or teams will be included in the report. Multiple values should be separated by commas. Example: `dev1, dev2, team1` | - | | `EXCLUDE_USERS` | Data for the specified users or teams will be excluded from the report. Multiple values should be separated by commas. Example: `dev1, dev2, team1` | - | +| `EXCLUDE_HEAD_BRANCHES` | PRs from head branches (source branches) matching the specified regular expression pattern will be excluded from the report. Example: `^hotfix` | - | +| `INCLUDE_HEAD_BRANCHES` | Only PRs from head branches (source branches) matching the specified regular expression pattern will be included in the report. Example: `^feature` | - | +| `EXCLUDE_BASE_BRANCHES` | PRs to base branches (target branches) matching the specified regular expression pattern will be excluded from the report. Example: `^main$` | - | +| `INCLUDE_BASE_BRANCHES` | Only PRs to base branches (target branches) matching the specified regular expression pattern will be included in the report. Example: `^develop$` | - | | `EXECUTION_OUTCOME` | This parameter allows you to specify the format in which you wish to receive the report. Options include creating a new issue, updating an existing one, obtaining markdown, or JSON. Markdown and JSON will be available in outputs. Can take mulitple values separated by commas: `new-issue`, `markdown`, `collection`, `existing-issue`. This parameter is **required** Example: `existing-issue` | `new-issue` | | `ISSUE_NUMBER` | Issue number to update. Add `existing-issue` to `EXECUTION_OUTCOME` for updating existing issue. The specified issue must already exist at the time the action is executed. This parameter is mandatory if the `EXECUTION_OUTCOME` input includes `existing-issue` value | - | | `ALLOW_ANALYTICS` | Allows sending non-sensitive inputs to mixpanel for better understanding user's needs. Set the value to `false` to disable sending action parameter data | `true` | @@ -305,6 +309,7 @@ Below is a table describing the possible outputs of **pull-request-analytics-act - To hide individual metrics, specify users in the `HIDE_USERS` parameter or leave `total` and GitHub team names in the `SHOW_USERS` parameter. - To avoid a long list of title changes when updating an existing issue, it is recommended to set the title yourself using the `ISSUE_TITLE` parameter. - You can filter pull requests using labels with the `EXCLUDE_LABELS` and `INCLUDE_LABELS` parameters. +- You can filter pull requests by both source (head) and target (base) branches using the `EXCLUDE_HEAD_BRANCHES`, `INCLUDE_HEAD_BRANCHES`, `EXCLUDE_BASE_BRANCHES`, and `INCLUDE_BASE_BRANCHES` parameters. These parameters accept regular expression patterns for flexible filtering. Note that exclude filters always take priority over include filters. ## Troubleshooting diff --git a/action.yml b/action.yml index c224873..ccfc562 100644 --- a/action.yml +++ b/action.yml @@ -100,6 +100,18 @@ inputs: EXCLUDE_LABELS: description: "Excludes PRs with mentioned labels. Values should be separated by comma" required: false + INCLUDE_HEAD_BRANCHES: + description: "Includes only PRs from head branches matching the specified regular expression pattern" + required: false + EXCLUDE_HEAD_BRANCHES: + description: "Excludes PRs from head branches matching the specified regular expression pattern" + required: false + INCLUDE_BASE_BRANCHES: + description: "Includes only PRs to base branches matching the specified regular expression pattern" + required: false + EXCLUDE_BASE_BRANCHES: + description: "Excludes PRs to base branches matching the specified regular expression pattern" + required: false INCLUDE_USERS: description: "Only data for the specified users will be included in the report. Multiple values should be separated by commas" required: false diff --git a/build/index.js b/build/index.js index 2a57f97..901b5f0 100644 --- a/build/index.js +++ b/build/index.js @@ -2419,23 +2419,19 @@ exports.makeComplexRequest = void 0; const utils_1 = __nccwpck_require__(41002); const getDataWithThrottle_1 = __nccwpck_require__(17227); const getPullRequests_1 = __nccwpck_require__(21341); +const utils_2 = __nccwpck_require__(50426); const makeComplexRequest = async (amount = 100, repository, options = { skipComments: true, }) => { const pullRequests = await (0, getPullRequests_1.getPullRequests)(amount, repository); - const pullRequestNumbers = pullRequests - .filter((pr) => { - const excludeLabels = (0, utils_1.getMultipleValuesInput)("EXCLUDE_LABELS"); - const includeLabels = (0, utils_1.getMultipleValuesInput)("INCLUDE_LABELS"); - const isIncludeLabelsCorrect = includeLabels.length > 0 - ? pr.labels.some((label) => includeLabels.includes(label.name)) - : true; - const isExcludeLabelsCorrect = excludeLabels.length > 0 - ? !pr.labels.some((label) => excludeLabels.includes(label.name)) - : true; - return isIncludeLabelsCorrect && isExcludeLabelsCorrect; - }) - .map((item) => item.number); + const pullRequestNumbers = (0, utils_2.filterPRs)(pullRequests, { + excludeLabels: (0, utils_1.getMultipleValuesInput)("EXCLUDE_LABELS"), + includeLabels: (0, utils_1.getMultipleValuesInput)("INCLUDE_LABELS"), + excludeHeadBranchesPattern: (0, utils_1.getValueAsIs)("EXCLUDE_HEAD_BRANCHES"), + includeHeadBranchesPattern: (0, utils_1.getValueAsIs)("INCLUDE_HEAD_BRANCHES"), + excludeBaseBranchesPattern: (0, utils_1.getValueAsIs)("EXCLUDE_BASE_BRANCHES"), + includeBaseBranchesPattern: (0, utils_1.getValueAsIs)("INCLUDE_BASE_BRANCHES"), + }); const { PRs, PREvents, PRComments } = await (0, getDataWithThrottle_1.getDataWithThrottle)(pullRequestNumbers, repository, options); const events = PREvents.map((element) => element.status === "fulfilled" ? element.value.data : null); const pullRequestInfo = PRs.map((element) => element.status === "fulfilled" ? element.value.data : null); @@ -2450,6 +2446,57 @@ const makeComplexRequest = async (amount = 100, repository, options = { exports.makeComplexRequest = makeComplexRequest; +/***/ }), + +/***/ 13975: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.filterPRs = void 0; +const filterPRs = (pullRequests, { excludeLabels, includeLabels, excludeHeadBranchesPattern, includeHeadBranchesPattern, excludeBaseBranchesPattern, includeBaseBranchesPattern, }) => { + return pullRequests + .filter((pr) => { + // Check all exclude conditions first - if any match, exclude the PR + const hasExcludedLabel = excludeLabels.length > 0 && + pr.labels.some((label) => excludeLabels.includes(label.name)); + const hasExcludedHeadBranch = excludeHeadBranchesPattern.length > 0 && + new RegExp(excludeHeadBranchesPattern).test(pr.head.ref); + const hasExcludedBaseBranch = excludeBaseBranchesPattern.length > 0 && + new RegExp(excludeBaseBranchesPattern).test(pr.base.ref); + // If any exclude condition matches, reject the PR immediately + if (hasExcludedLabel || hasExcludedHeadBranch || hasExcludedBaseBranch) { + return false; + } + if (includeLabels.length === 0 && + includeBaseBranchesPattern.length === 0 && + includeHeadBranchesPattern.length === 0) { + return true; + } + // Check include conditions - ALL specified types must match (AND logic) + // If include labels are specified, PR must have at least one + const matchesIncludeLabels = includeLabels.length === 0 + ? false + : pr.labels.some((label) => includeLabels.includes(label.name)); + // If include head branch pattern is specified, PR must match + const matchesIncludeHeadBranch = includeHeadBranchesPattern.length === 0 + ? false + : new RegExp(includeHeadBranchesPattern).test(pr.head.ref); + // If include base branch pattern is specified, PR must match + const matchesIncludeBaseBranch = includeBaseBranchesPattern.length === 0 + ? false + : new RegExp(includeBaseBranchesPattern).test(pr.base.ref); + // All specified include conditions must be satisfied + return (matchesIncludeLabels || + matchesIncludeHeadBranch || + matchesIncludeBaseBranch); + }) + .map((item) => item.number); +}; +exports.filterPRs = filterPRs; + + /***/ }), /***/ 57288: @@ -2513,11 +2560,13 @@ exports.getReportDates = getReportDates; "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.getOwnersRepositories = exports.getReportDates = void 0; +exports.filterPRs = exports.getOwnersRepositories = exports.getReportDates = void 0; var getReportDates_1 = __nccwpck_require__(30183); Object.defineProperty(exports, "getReportDates", ({ enumerable: true, get: function () { return getReportDates_1.getReportDates; } })); var getOwnersRepositories_1 = __nccwpck_require__(57288); Object.defineProperty(exports, "getOwnersRepositories", ({ enumerable: true, get: function () { return getOwnersRepositories_1.getOwnersRepositories; } })); +var filterPRs_1 = __nccwpck_require__(13975); +Object.defineProperty(exports, "filterPRs", ({ enumerable: true, get: function () { return filterPRs_1.filterPRs; } })); /***/ }), @@ -2917,6 +2966,10 @@ ${[ "USE_CHARTS", "INCLUDE_LABELS", "EXCLUDE_LABELS", + "INCLUDE_HEAD_BRANCHES", + "EXCLUDE_HEAD_BRANCHES", + "INCLUDE_BASE_BRANCHES", + "EXCLUDE_BASE_BRANCHES", "INCLUDE_USERS", "EXCLUDE_USERS", "EXECUTION_OUTCOME", diff --git a/package-lock.json b/package-lock.json index 364e986..dd6a925 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pull-request-analytics-action", - "version": "4.8.1", + "version": "4.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pull-request-analytics-action", - "version": "4.8.1", + "version": "4.9.0", "license": "MIT", "dependencies": { "@actions/core": "^1.10.1", diff --git a/package.json b/package.json index 8b33c50..8e81710 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pull-request-analytics-action", - "version": "4.8.1", + "version": "4.9.0", "description": "Generates detailed PR analytics reports within GitHub, focusing on review efficiency and team performance.", "main": "build/index.js", "scripts": { diff --git a/src/requests/makeComplexRequest.ts b/src/requests/makeComplexRequest.ts index 243dd40..2dfbc58 100644 --- a/src/requests/makeComplexRequest.ts +++ b/src/requests/makeComplexRequest.ts @@ -1,7 +1,8 @@ -import { getMultipleValuesInput } from "../common/utils"; +import { getMultipleValuesInput, getValueAsIs } from "../common/utils"; import { getDataWithThrottle } from "./getDataWithThrottle"; import { getPullRequests } from "./getPullRequests"; import { Options, Repository } from "./types"; +import { filterPRs } from "./utils"; export const makeComplexRequest = async ( amount: number = 100, @@ -12,21 +13,14 @@ export const makeComplexRequest = async ( ) => { const pullRequests = await getPullRequests(amount, repository); - const pullRequestNumbers = pullRequests - .filter((pr) => { - const excludeLabels = getMultipleValuesInput("EXCLUDE_LABELS"); - const includeLabels = getMultipleValuesInput("INCLUDE_LABELS"); - const isIncludeLabelsCorrect = - includeLabels.length > 0 - ? pr.labels.some((label) => includeLabels.includes(label.name)) - : true; - const isExcludeLabelsCorrect = - excludeLabels.length > 0 - ? !pr.labels.some((label) => excludeLabels.includes(label.name)) - : true; - return isIncludeLabelsCorrect && isExcludeLabelsCorrect; - }) - .map((item) => item.number); + const pullRequestNumbers = filterPRs(pullRequests, { + excludeLabels: getMultipleValuesInput("EXCLUDE_LABELS"), + includeLabels: getMultipleValuesInput("INCLUDE_LABELS"), + excludeHeadBranchesPattern: getValueAsIs("EXCLUDE_HEAD_BRANCHES"), + includeHeadBranchesPattern: getValueAsIs("INCLUDE_HEAD_BRANCHES"), + excludeBaseBranchesPattern: getValueAsIs("EXCLUDE_BASE_BRANCHES"), + includeBaseBranchesPattern: getValueAsIs("INCLUDE_BASE_BRANCHES"), + }); const { PRs, PREvents, PRComments } = await getDataWithThrottle( pullRequestNumbers, diff --git a/src/requests/utils/filterPRs.mock.ts b/src/requests/utils/filterPRs.mock.ts new file mode 100644 index 0000000..c7f66ba --- /dev/null +++ b/src/requests/utils/filterPRs.mock.ts @@ -0,0 +1,70 @@ +// Mock data: 10 PRs with different branches and labels +export const mockPullRequests = [ + // PRs with fix/ branch + { + number: 1, + head: { ref: "fix/authentication-bug" }, + base: { ref: "main" }, + labels: [{ name: "bug" }, { name: "enhancement" }], + }, + { + number: 2, + head: { ref: "fix/ui-glitch" }, + base: { ref: "develop" }, + labels: [{ name: "ui-kit" }, { name: "bug" }], + }, + { + number: 3, + head: { ref: "fix/validation-error" }, + base: { ref: "main" }, + labels: [], + }, + + // PRs with feature/ branch + { + number: 4, + head: { ref: "feature/user-dashboard" }, + base: { ref: "develop" }, + labels: [{ name: "enhancement" }, { name: "ui-kit" }], + }, + { + number: 5, + head: { ref: "feature/export-functionality" }, + base: { ref: "develop" }, + labels: [{ name: "enhancement" }], + }, + { + number: 6, + head: { ref: "feature/user-profile-page" }, + base: { ref: "main" }, + labels: [{ name: "enhancement" }, { name: "docs" }], + }, + + // PRs with refactor/ branch + { + number: 7, + head: { ref: "refactor/authentication-module" }, + base: { ref: "develop" }, + labels: [{ name: "enhancement" }], + }, + { + number: 8, + head: { ref: "refactor/api-endpoints" }, + base: { ref: "main" }, + labels: [], + }, + + // PRs with cursor/ branch + { + number: 9, + head: { ref: "cursor/code-improvements" }, + base: { ref: "main" }, + labels: [{ name: "enhancement" }], + }, + { + number: 10, + head: { ref: "cursor/ui-fixes" }, + base: { ref: "develop" }, + labels: [{ name: "ui-kit" }, { name: "bug" }], + }, +]; diff --git a/src/requests/utils/filterPRs.spec.ts b/src/requests/utils/filterPRs.spec.ts new file mode 100644 index 0000000..5685a1c --- /dev/null +++ b/src/requests/utils/filterPRs.spec.ts @@ -0,0 +1,328 @@ +import { filterPRs } from "./filterPRs"; +import { mockPullRequests } from "./filterPRs.mock"; + +describe("filterPRs", () => { + describe("Label filtering", () => { + it("should return all PRs when no filters are provided", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + excludeHeadBranchesPattern: "", + includeHeadBranchesPattern: "", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + }); + + it("should filter PRs by include labels (bug)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: ["bug"], + excludeHeadBranchesPattern: "", + includeHeadBranchesPattern: "", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + expect(result).toEqual([1, 2, 10]); + }); + + it("should filter PRs by multiple include labels (bug OR enhancement)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: ["bug", "enhancement"], + excludeHeadBranchesPattern: "", + includeHeadBranchesPattern: "", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + // PRs that have at least one of the labels + expect(result).toEqual([1, 2, 4, 5, 6, 7, 9, 10]); + }); + + it("should filter PRs by exclude labels (bug)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: ["bug"], + includeLabels: [], + excludeHeadBranchesPattern: "", + includeHeadBranchesPattern: "", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + // All PRs except those with "bug" label + expect(result).toEqual([3, 4, 5, 6, 7, 8, 9]); + }); + + it("should filter PRs by multiple exclude labels (bug AND ui-kit)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: ["bug", "ui-kit"], + includeLabels: [], + excludeHeadBranchesPattern: "", + includeHeadBranchesPattern: "", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + // PRs that don't have any of the excluded labels + expect(result).toEqual([3, 5, 6, 7, 8, 9]); + }); + + it("should filter PRs with both include and exclude labels", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: ["bug"], + includeLabels: ["enhancement"], + excludeHeadBranchesPattern: "", + includeHeadBranchesPattern: "", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + // PRs that have "enhancement" but not "bug" + expect(result).toEqual([4, 5, 6, 7, 9]); + }); + + it("should return only PRs without any labels when including non-existent label", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: ["non-existent-label"], + excludeHeadBranchesPattern: "", + includeHeadBranchesPattern: "", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + expect(result).toEqual([]); + }); + + it("should return all PRs when excluding non-existent label", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: ["non-existent-label"], + includeLabels: [], + excludeHeadBranchesPattern: "", + includeHeadBranchesPattern: "", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + }); + }); + + describe("Branch filtering", () => { + it("should filter PRs by include branch pattern (fix/)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + excludeHeadBranchesPattern: "", + includeHeadBranchesPattern: "^fix/", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + expect(result).toEqual([1, 2, 3]); + }); + + it("should filter PRs by multiple branch patterns (fix/ OR feature/)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + excludeHeadBranchesPattern: "", + includeHeadBranchesPattern: "^(fix|feature)/", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + expect(result).toEqual([1, 2, 3, 4, 5, 6]); + }); + + it("should filter PRs by exclude branch pattern (fix/)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + excludeHeadBranchesPattern: "^fix/", + includeHeadBranchesPattern: "", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + // All PRs except those starting with "fix/" + expect(result).toEqual([4, 5, 6, 7, 8, 9, 10]); + }); + + it("should filter PRs by multiple exclude branch patterns (cursor/ AND refactor/)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + excludeHeadBranchesPattern: "^(cursor|refactor)/", + includeHeadBranchesPattern: "", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + // PRs that don't start with "cursor/" or "refactor/" + expect(result).toEqual([1, 2, 3, 4, 5, 6]); + }); + + it("should filter PRs with both include and exclude branch patterns", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + excludeHeadBranchesPattern: "dashboard", + includeHeadBranchesPattern: "^feature/", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + // PRs that start with "feature/" but don't contain "dashboard" + expect(result).toEqual([5, 6]); + }); + + it("should filter PRs by branch pattern containing specific keyword", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + excludeHeadBranchesPattern: "", + includeHeadBranchesPattern: "ui", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + // PRs with "ui" in branch name + expect(result).toEqual([2, 10]); + }); + }); + + describe("Combined filtering (labels + branches)", () => { + it("should filter PRs by include label and include branch pattern", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: ["bug"], + excludeHeadBranchesPattern: "", + includeHeadBranchesPattern: "^fix/", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + // PRs with "bug" label AND starting with "fix/" + expect(result).toEqual([1, 2, 3, 10]); + }); + + it("should filter PRs by include label and exclude branch pattern", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: ["ui-kit"], + excludeHeadBranchesPattern: "^cursor/", + includeHeadBranchesPattern: "", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + // PRs with "ui-kit" label AND not starting with "cursor/" + expect(result).toEqual([2, 4]); + }); + + it("should filter PRs by exclude label and include branch pattern", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: ["bug"], + includeLabels: [], + excludeHeadBranchesPattern: "", + includeHeadBranchesPattern: "^feature/", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + // PRs starting with "feature/" AND not having "bug" label + expect(result).toEqual([4, 5, 6]); + }); + + it("should filter PRs by exclude label and exclude branch pattern", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: ["docs"], + includeLabels: [], + excludeHeadBranchesPattern: "^fix/", + includeHeadBranchesPattern: "", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + // PRs not starting with "fix/" AND not having "docs" label + expect(result).toEqual([4, 5, 7, 8, 9, 10]); + }); + + it("should filter PRs with all filters applied", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: ["bug"], + includeLabels: ["enhancement"], + excludeHeadBranchesPattern: "^cursor/", + includeHeadBranchesPattern: "^(feature|refactor)/", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + // PRs with "enhancement" label, without "bug" label, + // starting with "feature/" or "refactor/", and not starting with "cursor/" + expect(result).toEqual([4, 5, 6, 7, 8]); + }); + + it("should return empty array when filters exclude all PRs", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: ["non-existent"], + excludeHeadBranchesPattern: "", + includeHeadBranchesPattern: "^non-existent/", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "", + }); + + expect(result).toEqual([]); + }); + }); + + describe("Base branch filtering", () => { + it("should filter PRs by include base branch pattern (main)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + excludeHeadBranchesPattern: "", + includeHeadBranchesPattern: "", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "^main$", + }); + + // PRs with base branch = main + expect(result).toEqual([1, 3, 6, 8, 9]); + }); + + it("should filter PRs by exclude base branch pattern (main)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + excludeHeadBranchesPattern: "", + includeHeadBranchesPattern: "", + excludeBaseBranchesPattern: "^main$", + includeBaseBranchesPattern: "", + }); + + // All PRs except those with base branch = main + expect(result).toEqual([2, 4, 5, 7, 10]); + }); + + it("should filter PRs by both head and base branch patterns", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + excludeHeadBranchesPattern: "", + includeHeadBranchesPattern: "^feature/", + excludeBaseBranchesPattern: "", + includeBaseBranchesPattern: "^develop$", + }); + + // PRs starting with "feature/" OR with base branch = develop + expect(result).toEqual([2, 4, 5, 6, 7, 10]); + }); + }); +}); diff --git a/src/requests/utils/filterPRs.ts b/src/requests/utils/filterPRs.ts new file mode 100644 index 0000000..b82fbb0 --- /dev/null +++ b/src/requests/utils/filterPRs.ts @@ -0,0 +1,79 @@ +export const filterPRs = ( + pullRequests: { + labels: { name: string }[]; + head: { ref: string }; + base: { ref: string }; + number: number; + }[], + { + excludeLabels, + includeLabels, + excludeHeadBranchesPattern, + includeHeadBranchesPattern, + excludeBaseBranchesPattern, + includeBaseBranchesPattern, + }: { + excludeLabels: string[]; + includeLabels: string[]; + excludeHeadBranchesPattern: string; + includeHeadBranchesPattern: string; + excludeBaseBranchesPattern: string; + includeBaseBranchesPattern: string; + } +) => { + return pullRequests + .filter((pr) => { + // Check all exclude conditions first - if any match, exclude the PR + const hasExcludedLabel = + excludeLabels.length > 0 && + pr.labels.some((label) => excludeLabels.includes(label.name)); + + const hasExcludedHeadBranch = + excludeHeadBranchesPattern.length > 0 && + new RegExp(excludeHeadBranchesPattern).test(pr.head.ref); + + const hasExcludedBaseBranch = + excludeBaseBranchesPattern.length > 0 && + new RegExp(excludeBaseBranchesPattern).test(pr.base.ref); + + // If any exclude condition matches, reject the PR immediately + if (hasExcludedLabel || hasExcludedHeadBranch || hasExcludedBaseBranch) { + return false; + } + + if ( + includeLabels.length === 0 && + includeBaseBranchesPattern.length === 0 && + includeHeadBranchesPattern.length === 0 + ) { + return true; + } + + // Check include conditions - ALL specified types must match (AND logic) + // If include labels are specified, PR must have at least one + const matchesIncludeLabels = + includeLabels.length === 0 + ? false + : pr.labels.some((label) => includeLabels.includes(label.name)); + + // If include head branch pattern is specified, PR must match + const matchesIncludeHeadBranch = + includeHeadBranchesPattern.length === 0 + ? false + : new RegExp(includeHeadBranchesPattern).test(pr.head.ref); + + // If include base branch pattern is specified, PR must match + const matchesIncludeBaseBranch = + includeBaseBranchesPattern.length === 0 + ? false + : new RegExp(includeBaseBranchesPattern).test(pr.base.ref); + + // All specified include conditions must be satisfied + return ( + matchesIncludeLabels || + matchesIncludeHeadBranch || + matchesIncludeBaseBranch + ); + }) + .map((item) => item.number); +}; diff --git a/src/requests/utils/index.ts b/src/requests/utils/index.ts index ee8b8b6..f1808a8 100644 --- a/src/requests/utils/index.ts +++ b/src/requests/utils/index.ts @@ -1,2 +1,3 @@ export { getReportDates } from "./getReportDates"; export { getOwnersRepositories } from "./getOwnersRepositories"; +export { filterPRs } from "./filterPRs"; diff --git a/src/view/utils/createConfigParamsCode.ts b/src/view/utils/createConfigParamsCode.ts index 1d5c230..28c0476 100644 --- a/src/view/utils/createConfigParamsCode.ts +++ b/src/view/utils/createConfigParamsCode.ts @@ -36,6 +36,10 @@ ${[ "USE_CHARTS", "INCLUDE_LABELS", "EXCLUDE_LABELS", + "INCLUDE_HEAD_BRANCHES", + "EXCLUDE_HEAD_BRANCHES", + "INCLUDE_BASE_BRANCHES", + "EXCLUDE_BASE_BRANCHES", "INCLUDE_USERS", "EXCLUDE_USERS", "EXECUTION_OUTCOME", From e99d5e137474618943f653a6f734e6768899e072 Mon Sep 17 00:00:00 2001 From: Aleksei Simatov Date: Tue, 2 Dec 2025 01:44:19 +0700 Subject: [PATCH 2/3] Feature: Filtering by branch name(Change parameters) --- README.md | 6 +- action.yml | 10 +- build/index.js | 63 ++++------ src/requests/makeComplexRequest.ts | 6 +- src/requests/utils/filterPRs.spec.ts | 144 ++++++++--------------- src/requests/utils/filterPRs.ts | 81 ++++--------- src/view/utils/createConfigParamsCode.ts | 6 +- 7 files changed, 103 insertions(+), 213 deletions(-) diff --git a/README.md b/README.md index 557a338..f5876d1 100644 --- a/README.md +++ b/README.md @@ -282,10 +282,8 @@ Below is a table outlining the various configuration parameters available for ** | `INCLUDE_LABELS` | Only PRs with mentioned labels will be included in the report. Values should be separated by commas. Example: `bugfix, enhancement` | - | | `INCLUDE_USERS` | Only data for the specified users or teams will be included in the report. Multiple values should be separated by commas. Example: `dev1, dev2, team1` | - | | `EXCLUDE_USERS` | Data for the specified users or teams will be excluded from the report. Multiple values should be separated by commas. Example: `dev1, dev2, team1` | - | -| `EXCLUDE_HEAD_BRANCHES` | PRs from head branches (source branches) matching the specified regular expression pattern will be excluded from the report. Example: `^hotfix` | - | -| `INCLUDE_HEAD_BRANCHES` | Only PRs from head branches (source branches) matching the specified regular expression pattern will be included in the report. Example: `^feature` | - | -| `EXCLUDE_BASE_BRANCHES` | PRs to base branches (target branches) matching the specified regular expression pattern will be excluded from the report. Example: `^main$` | - | -| `INCLUDE_BASE_BRANCHES` | Only PRs to base branches (target branches) matching the specified regular expression pattern will be included in the report. Example: `^develop$` | - | +| `FILTER_HEAD_BRANCHES` | Only PRs from head branches (source branches) matching the specified regular expression pattern will be included in the report. Example: `^feature` | - | +| `FILTER_BASE_BRANCHES` | Only PRs to base branches (target branches) matching the specified regular expression pattern will be included in the report. Example: `^develop$` | - | | `EXECUTION_OUTCOME` | This parameter allows you to specify the format in which you wish to receive the report. Options include creating a new issue, updating an existing one, obtaining markdown, or JSON. Markdown and JSON will be available in outputs. Can take mulitple values separated by commas: `new-issue`, `markdown`, `collection`, `existing-issue`. This parameter is **required** Example: `existing-issue` | `new-issue` | | `ISSUE_NUMBER` | Issue number to update. Add `existing-issue` to `EXECUTION_OUTCOME` for updating existing issue. The specified issue must already exist at the time the action is executed. This parameter is mandatory if the `EXECUTION_OUTCOME` input includes `existing-issue` value | - | | `ALLOW_ANALYTICS` | Allows sending non-sensitive inputs to mixpanel for better understanding user's needs. Set the value to `false` to disable sending action parameter data | `true` | diff --git a/action.yml b/action.yml index ccfc562..89116b7 100644 --- a/action.yml +++ b/action.yml @@ -100,18 +100,12 @@ inputs: EXCLUDE_LABELS: description: "Excludes PRs with mentioned labels. Values should be separated by comma" required: false - INCLUDE_HEAD_BRANCHES: + FILTER_HEAD_BRANCHES: description: "Includes only PRs from head branches matching the specified regular expression pattern" required: false - EXCLUDE_HEAD_BRANCHES: - description: "Excludes PRs from head branches matching the specified regular expression pattern" - required: false - INCLUDE_BASE_BRANCHES: + FILTER_BASE_BRANCHES: description: "Includes only PRs to base branches matching the specified regular expression pattern" required: false - EXCLUDE_BASE_BRANCHES: - description: "Excludes PRs to base branches matching the specified regular expression pattern" - required: false INCLUDE_USERS: description: "Only data for the specified users will be included in the report. Multiple values should be separated by commas" required: false diff --git a/build/index.js b/build/index.js index 901b5f0..5894f09 100644 --- a/build/index.js +++ b/build/index.js @@ -2427,10 +2427,8 @@ const makeComplexRequest = async (amount = 100, repository, options = { const pullRequestNumbers = (0, utils_2.filterPRs)(pullRequests, { excludeLabels: (0, utils_1.getMultipleValuesInput)("EXCLUDE_LABELS"), includeLabels: (0, utils_1.getMultipleValuesInput)("INCLUDE_LABELS"), - excludeHeadBranchesPattern: (0, utils_1.getValueAsIs)("EXCLUDE_HEAD_BRANCHES"), - includeHeadBranchesPattern: (0, utils_1.getValueAsIs)("INCLUDE_HEAD_BRANCHES"), - excludeBaseBranchesPattern: (0, utils_1.getValueAsIs)("EXCLUDE_BASE_BRANCHES"), - includeBaseBranchesPattern: (0, utils_1.getValueAsIs)("INCLUDE_BASE_BRANCHES"), + filterHeadBranchesPattern: (0, utils_1.getValueAsIs)("FILTER_HEAD_BRANCHES"), + filterBaseBranchesPattern: (0, utils_1.getValueAsIs)("FILTER_BASE_BRANCHES"), }); const { PRs, PREvents, PRComments } = await (0, getDataWithThrottle_1.getDataWithThrottle)(pullRequestNumbers, repository, options); const events = PREvents.map((element) => element.status === "fulfilled" ? element.value.data : null); @@ -2455,42 +2453,25 @@ exports.makeComplexRequest = makeComplexRequest; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.filterPRs = void 0; -const filterPRs = (pullRequests, { excludeLabels, includeLabels, excludeHeadBranchesPattern, includeHeadBranchesPattern, excludeBaseBranchesPattern, includeBaseBranchesPattern, }) => { +const filterPRs = (pullRequests, { excludeLabels, includeLabels, filterHeadBranchesPattern, filterBaseBranchesPattern, }) => { return pullRequests .filter((pr) => { - // Check all exclude conditions first - if any match, exclude the PR - const hasExcludedLabel = excludeLabels.length > 0 && - pr.labels.some((label) => excludeLabels.includes(label.name)); - const hasExcludedHeadBranch = excludeHeadBranchesPattern.length > 0 && - new RegExp(excludeHeadBranchesPattern).test(pr.head.ref); - const hasExcludedBaseBranch = excludeBaseBranchesPattern.length > 0 && - new RegExp(excludeBaseBranchesPattern).test(pr.base.ref); - // If any exclude condition matches, reject the PR immediately - if (hasExcludedLabel || hasExcludedHeadBranch || hasExcludedBaseBranch) { - return false; - } - if (includeLabels.length === 0 && - includeBaseBranchesPattern.length === 0 && - includeHeadBranchesPattern.length === 0) { - return true; - } - // Check include conditions - ALL specified types must match (AND logic) - // If include labels are specified, PR must have at least one - const matchesIncludeLabels = includeLabels.length === 0 - ? false - : pr.labels.some((label) => includeLabels.includes(label.name)); - // If include head branch pattern is specified, PR must match - const matchesIncludeHeadBranch = includeHeadBranchesPattern.length === 0 - ? false - : new RegExp(includeHeadBranchesPattern).test(pr.head.ref); - // If include base branch pattern is specified, PR must match - const matchesIncludeBaseBranch = includeBaseBranchesPattern.length === 0 - ? false - : new RegExp(includeBaseBranchesPattern).test(pr.base.ref); - // All specified include conditions must be satisfied - return (matchesIncludeLabels || - matchesIncludeHeadBranch || - matchesIncludeBaseBranch); + const isIncludeLabelsCorrect = includeLabels.length > 0 + ? pr.labels.some((label) => includeLabels.includes(label.name)) + : true; + const isExcludeLabelsCorrect = excludeLabels.length > 0 + ? !pr.labels.some((label) => excludeLabels.includes(label.name)) + : true; + const isFilterHeadBranchesCorrect = filterHeadBranchesPattern.length > 0 + ? new RegExp(filterHeadBranchesPattern).test(pr.head.ref) + : true; + const isFilterBaseBranchesCorrect = filterBaseBranchesPattern.length > 0 + ? new RegExp(filterBaseBranchesPattern).test(pr.base.ref) + : true; + return (isIncludeLabelsCorrect && + isExcludeLabelsCorrect && + isFilterHeadBranchesCorrect && + isFilterBaseBranchesCorrect); }) .map((item) => item.number); }; @@ -2966,10 +2947,8 @@ ${[ "USE_CHARTS", "INCLUDE_LABELS", "EXCLUDE_LABELS", - "INCLUDE_HEAD_BRANCHES", - "EXCLUDE_HEAD_BRANCHES", - "INCLUDE_BASE_BRANCHES", - "EXCLUDE_BASE_BRANCHES", + "FILTER_HEAD_BRANCHES", + "FILTER_BASE_BRANCHES", "INCLUDE_USERS", "EXCLUDE_USERS", "EXECUTION_OUTCOME", diff --git a/src/requests/makeComplexRequest.ts b/src/requests/makeComplexRequest.ts index 2dfbc58..4e540e8 100644 --- a/src/requests/makeComplexRequest.ts +++ b/src/requests/makeComplexRequest.ts @@ -16,10 +16,8 @@ export const makeComplexRequest = async ( const pullRequestNumbers = filterPRs(pullRequests, { excludeLabels: getMultipleValuesInput("EXCLUDE_LABELS"), includeLabels: getMultipleValuesInput("INCLUDE_LABELS"), - excludeHeadBranchesPattern: getValueAsIs("EXCLUDE_HEAD_BRANCHES"), - includeHeadBranchesPattern: getValueAsIs("INCLUDE_HEAD_BRANCHES"), - excludeBaseBranchesPattern: getValueAsIs("EXCLUDE_BASE_BRANCHES"), - includeBaseBranchesPattern: getValueAsIs("INCLUDE_BASE_BRANCHES"), + filterHeadBranchesPattern: getValueAsIs("FILTER_HEAD_BRANCHES"), + filterBaseBranchesPattern: getValueAsIs("FILTER_BASE_BRANCHES"), }); const { PRs, PREvents, PRComments } = await getDataWithThrottle( diff --git a/src/requests/utils/filterPRs.spec.ts b/src/requests/utils/filterPRs.spec.ts index 5685a1c..822c519 100644 --- a/src/requests/utils/filterPRs.spec.ts +++ b/src/requests/utils/filterPRs.spec.ts @@ -7,10 +7,8 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: [], includeLabels: [], - excludeHeadBranchesPattern: "", - includeHeadBranchesPattern: "", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterHeadBranchesPattern: "", + filterBaseBranchesPattern: "", }); expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); @@ -20,10 +18,8 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: [], includeLabels: ["bug"], - excludeHeadBranchesPattern: "", - includeHeadBranchesPattern: "", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterBaseBranchesPattern: "", + filterHeadBranchesPattern: "", }); expect(result).toEqual([1, 2, 10]); @@ -33,10 +29,8 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: [], includeLabels: ["bug", "enhancement"], - excludeHeadBranchesPattern: "", - includeHeadBranchesPattern: "", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterBaseBranchesPattern: "", + filterHeadBranchesPattern: "", }); // PRs that have at least one of the labels @@ -47,10 +41,8 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: ["bug"], includeLabels: [], - excludeHeadBranchesPattern: "", - includeHeadBranchesPattern: "", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterBaseBranchesPattern: "", + filterHeadBranchesPattern: "", }); // All PRs except those with "bug" label @@ -61,10 +53,8 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: ["bug", "ui-kit"], includeLabels: [], - excludeHeadBranchesPattern: "", - includeHeadBranchesPattern: "", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterBaseBranchesPattern: "", + filterHeadBranchesPattern: "", }); // PRs that don't have any of the excluded labels @@ -75,10 +65,8 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: ["bug"], includeLabels: ["enhancement"], - excludeHeadBranchesPattern: "", - includeHeadBranchesPattern: "", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterBaseBranchesPattern: "", + filterHeadBranchesPattern: "", }); // PRs that have "enhancement" but not "bug" @@ -89,10 +77,8 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: [], includeLabels: ["non-existent-label"], - excludeHeadBranchesPattern: "", - includeHeadBranchesPattern: "", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterBaseBranchesPattern: "", + filterHeadBranchesPattern: "", }); expect(result).toEqual([]); @@ -102,10 +88,8 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: ["non-existent-label"], includeLabels: [], - excludeHeadBranchesPattern: "", - includeHeadBranchesPattern: "", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterBaseBranchesPattern: "", + filterHeadBranchesPattern: "", }); expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); @@ -117,10 +101,8 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: [], includeLabels: [], - excludeHeadBranchesPattern: "", - includeHeadBranchesPattern: "^fix/", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterHeadBranchesPattern: "^fix/", + filterBaseBranchesPattern: "", }); expect(result).toEqual([1, 2, 3]); @@ -130,10 +112,8 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: [], includeLabels: [], - excludeHeadBranchesPattern: "", - includeHeadBranchesPattern: "^(fix|feature)/", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterHeadBranchesPattern: "^(fix|feature)/", + filterBaseBranchesPattern: "", }); expect(result).toEqual([1, 2, 3, 4, 5, 6]); @@ -143,10 +123,8 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: [], includeLabels: [], - excludeHeadBranchesPattern: "^fix/", - includeHeadBranchesPattern: "", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterHeadBranchesPattern: "^(?!fix/)", + filterBaseBranchesPattern: "", }); // All PRs except those starting with "fix/" @@ -157,10 +135,8 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: [], includeLabels: [], - excludeHeadBranchesPattern: "^(cursor|refactor)/", - includeHeadBranchesPattern: "", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterHeadBranchesPattern: "^(?!cursor|refactor).*", + filterBaseBranchesPattern: "", }); // PRs that don't start with "cursor/" or "refactor/" @@ -171,10 +147,8 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: [], includeLabels: [], - excludeHeadBranchesPattern: "dashboard", - includeHeadBranchesPattern: "^feature/", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterHeadBranchesPattern: "^feature/(?!.*dashboard).*", + filterBaseBranchesPattern: "", }); // PRs that start with "feature/" but don't contain "dashboard" @@ -185,10 +159,8 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: [], includeLabels: [], - excludeHeadBranchesPattern: "", - includeHeadBranchesPattern: "ui", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterHeadBranchesPattern: "ui", + filterBaseBranchesPattern: "", }); // PRs with "ui" in branch name @@ -201,24 +173,20 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: [], includeLabels: ["bug"], - excludeHeadBranchesPattern: "", - includeHeadBranchesPattern: "^fix/", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterHeadBranchesPattern: "^fix/", + filterBaseBranchesPattern: "", }); // PRs with "bug" label AND starting with "fix/" - expect(result).toEqual([1, 2, 3, 10]); + expect(result).toEqual([1, 2]); }); it("should filter PRs by include label and exclude branch pattern", () => { const result = filterPRs(mockPullRequests, { excludeLabels: [], includeLabels: ["ui-kit"], - excludeHeadBranchesPattern: "^cursor/", - includeHeadBranchesPattern: "", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterHeadBranchesPattern: "^(?!cursor/).*", + filterBaseBranchesPattern: "", }); // PRs with "ui-kit" label AND not starting with "cursor/" @@ -229,10 +197,8 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: ["bug"], includeLabels: [], - excludeHeadBranchesPattern: "", - includeHeadBranchesPattern: "^feature/", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterHeadBranchesPattern: "^feature/", + filterBaseBranchesPattern: "", }); // PRs starting with "feature/" AND not having "bug" label @@ -243,10 +209,8 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: ["docs"], includeLabels: [], - excludeHeadBranchesPattern: "^fix/", - includeHeadBranchesPattern: "", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterHeadBranchesPattern: "^(?!fix/).*", + filterBaseBranchesPattern: "", }); // PRs not starting with "fix/" AND not having "docs" label @@ -257,25 +221,21 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: ["bug"], includeLabels: ["enhancement"], - excludeHeadBranchesPattern: "^cursor/", - includeHeadBranchesPattern: "^(feature|refactor)/", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterHeadBranchesPattern: "^(feature|refactor)/(?!cursor/).*", + filterBaseBranchesPattern: "", }); // PRs with "enhancement" label, without "bug" label, // starting with "feature/" or "refactor/", and not starting with "cursor/" - expect(result).toEqual([4, 5, 6, 7, 8]); + expect(result).toEqual([4, 5, 6, 7]); }); it("should return empty array when filters exclude all PRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: [], includeLabels: ["non-existent"], - excludeHeadBranchesPattern: "", - includeHeadBranchesPattern: "^non-existent/", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "", + filterHeadBranchesPattern: "^non-existent/", + filterBaseBranchesPattern: "", }); expect(result).toEqual([]); @@ -287,10 +247,8 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: [], includeLabels: [], - excludeHeadBranchesPattern: "", - includeHeadBranchesPattern: "", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "^main$", + filterHeadBranchesPattern: "", + filterBaseBranchesPattern: "^main$", }); // PRs with base branch = main @@ -301,10 +259,8 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: [], includeLabels: [], - excludeHeadBranchesPattern: "", - includeHeadBranchesPattern: "", - excludeBaseBranchesPattern: "^main$", - includeBaseBranchesPattern: "", + filterHeadBranchesPattern: "", + filterBaseBranchesPattern: "^(?!main$).*", }); // All PRs except those with base branch = main @@ -315,14 +271,12 @@ describe("filterPRs", () => { const result = filterPRs(mockPullRequests, { excludeLabels: [], includeLabels: [], - excludeHeadBranchesPattern: "", - includeHeadBranchesPattern: "^feature/", - excludeBaseBranchesPattern: "", - includeBaseBranchesPattern: "^develop$", + filterHeadBranchesPattern: "^feature/", + filterBaseBranchesPattern: "^develop$", }); // PRs starting with "feature/" OR with base branch = develop - expect(result).toEqual([2, 4, 5, 6, 7, 10]); + expect(result).toEqual([4, 5]); }); }); }); diff --git a/src/requests/utils/filterPRs.ts b/src/requests/utils/filterPRs.ts index b82fbb0..c349d56 100644 --- a/src/requests/utils/filterPRs.ts +++ b/src/requests/utils/filterPRs.ts @@ -8,71 +8,40 @@ export const filterPRs = ( { excludeLabels, includeLabels, - excludeHeadBranchesPattern, - includeHeadBranchesPattern, - excludeBaseBranchesPattern, - includeBaseBranchesPattern, + filterHeadBranchesPattern, + filterBaseBranchesPattern, }: { excludeLabels: string[]; includeLabels: string[]; - excludeHeadBranchesPattern: string; - includeHeadBranchesPattern: string; - excludeBaseBranchesPattern: string; - includeBaseBranchesPattern: string; + filterHeadBranchesPattern: string; + filterBaseBranchesPattern: string; } ) => { return pullRequests .filter((pr) => { - // Check all exclude conditions first - if any match, exclude the PR - const hasExcludedLabel = - excludeLabels.length > 0 && - pr.labels.some((label) => excludeLabels.includes(label.name)); + const isIncludeLabelsCorrect = + includeLabels.length > 0 + ? pr.labels.some((label) => includeLabels.includes(label.name)) + : true; + const isExcludeLabelsCorrect = + excludeLabels.length > 0 + ? !pr.labels.some((label) => excludeLabels.includes(label.name)) + : true; + + const isFilterHeadBranchesCorrect = + filterHeadBranchesPattern.length > 0 + ? new RegExp(filterHeadBranchesPattern).test(pr.head.ref) + : true; + const isFilterBaseBranchesCorrect = + filterBaseBranchesPattern.length > 0 + ? new RegExp(filterBaseBranchesPattern).test(pr.base.ref) + : true; - const hasExcludedHeadBranch = - excludeHeadBranchesPattern.length > 0 && - new RegExp(excludeHeadBranchesPattern).test(pr.head.ref); - - const hasExcludedBaseBranch = - excludeBaseBranchesPattern.length > 0 && - new RegExp(excludeBaseBranchesPattern).test(pr.base.ref); - - // If any exclude condition matches, reject the PR immediately - if (hasExcludedLabel || hasExcludedHeadBranch || hasExcludedBaseBranch) { - return false; - } - - if ( - includeLabels.length === 0 && - includeBaseBranchesPattern.length === 0 && - includeHeadBranchesPattern.length === 0 - ) { - return true; - } - - // Check include conditions - ALL specified types must match (AND logic) - // If include labels are specified, PR must have at least one - const matchesIncludeLabels = - includeLabels.length === 0 - ? false - : pr.labels.some((label) => includeLabels.includes(label.name)); - - // If include head branch pattern is specified, PR must match - const matchesIncludeHeadBranch = - includeHeadBranchesPattern.length === 0 - ? false - : new RegExp(includeHeadBranchesPattern).test(pr.head.ref); - - // If include base branch pattern is specified, PR must match - const matchesIncludeBaseBranch = - includeBaseBranchesPattern.length === 0 - ? false - : new RegExp(includeBaseBranchesPattern).test(pr.base.ref); - - // All specified include conditions must be satisfied return ( - matchesIncludeLabels || - matchesIncludeHeadBranch || - matchesIncludeBaseBranch + isIncludeLabelsCorrect && + isExcludeLabelsCorrect && + isFilterHeadBranchesCorrect && + isFilterBaseBranchesCorrect ); }) .map((item) => item.number); diff --git a/src/view/utils/createConfigParamsCode.ts b/src/view/utils/createConfigParamsCode.ts index 28c0476..19de44b 100644 --- a/src/view/utils/createConfigParamsCode.ts +++ b/src/view/utils/createConfigParamsCode.ts @@ -36,10 +36,8 @@ ${[ "USE_CHARTS", "INCLUDE_LABELS", "EXCLUDE_LABELS", - "INCLUDE_HEAD_BRANCHES", - "EXCLUDE_HEAD_BRANCHES", - "INCLUDE_BASE_BRANCHES", - "EXCLUDE_BASE_BRANCHES", + "FILTER_HEAD_BRANCHES", + "FILTER_BASE_BRANCHES", "INCLUDE_USERS", "EXCLUDE_USERS", "EXECUTION_OUTCOME", From 253a13f350a964b642edfc36451ad885063db2a9 Mon Sep 17 00:00:00 2001 From: Aleksei Simatov Date: Tue, 2 Dec 2025 01:49:45 +0700 Subject: [PATCH 3/3] Feature: analytics --- build/index.js | 4 ++++ src/analytics/sendActionError.ts | 2 ++ src/analytics/sendActionRun.ts | 2 ++ 3 files changed, 8 insertions(+) diff --git a/build/index.js b/build/index.js index 5894f09..38de7a0 100644 --- a/build/index.js +++ b/build/index.js @@ -78,6 +78,8 @@ const sendActionError = (error) => { USE_CHARTS: (0, utils_1.getValueAsIs)("USE_CHARTS"), SHOW_CORRELATION_GRAPHS: (0, utils_1.getValueAsIs)("SHOW_CORRELATION_GRAPHS"), SHOW_ACTIVITY_TIME_GRAPHS: (0, utils_1.getValueAsIs)("SHOW_ACTIVITY_TIME_GRAPHS"), + FILTER_HEAD_BRANCHES: !!(0, utils_1.getValueAsIs)("FILTER_HEAD_BRANCHES"), + FILTER_BASE_BRANCHES: !!(0, utils_1.getValueAsIs)("FILTER_BASE_BRANCHES"), }); } else { @@ -138,6 +140,8 @@ const sendActionRun = () => { USE_CHARTS: (0, utils_1.getValueAsIs)("USE_CHARTS"), SHOW_CORRELATION_GRAPHS: (0, utils_1.getValueAsIs)("SHOW_CORRELATION_GRAPHS"), SHOW_ACTIVITY_TIME_GRAPHS: (0, utils_1.getValueAsIs)("SHOW_ACTIVITY_TIME_GRAPHS"), + FILTER_HEAD_BRANCHES: !!(0, utils_1.getValueAsIs)("FILTER_HEAD_BRANCHES"), + FILTER_BASE_BRANCHES: !!(0, utils_1.getValueAsIs)("FILTER_BASE_BRANCHES"), }); } else { diff --git a/src/analytics/sendActionError.ts b/src/analytics/sendActionError.ts index be01bc7..31caac4 100644 --- a/src/analytics/sendActionError.ts +++ b/src/analytics/sendActionError.ts @@ -44,6 +44,8 @@ export const sendActionError = (error: Error) => { USE_CHARTS: getValueAsIs("USE_CHARTS"), SHOW_CORRELATION_GRAPHS: getValueAsIs("SHOW_CORRELATION_GRAPHS"), SHOW_ACTIVITY_TIME_GRAPHS: getValueAsIs("SHOW_ACTIVITY_TIME_GRAPHS"), + FILTER_HEAD_BRANCHES: !!getValueAsIs("FILTER_HEAD_BRANCHES"), + FILTER_BASE_BRANCHES: !!getValueAsIs("FILTER_BASE_BRANCHES"), }); } else { mixpanel.track("Anonymous action error", { distinct_id: "anonymous" }); diff --git a/src/analytics/sendActionRun.ts b/src/analytics/sendActionRun.ts index 4516c8a..79dd58b 100644 --- a/src/analytics/sendActionRun.ts +++ b/src/analytics/sendActionRun.ts @@ -47,6 +47,8 @@ export const sendActionRun = () => { USE_CHARTS: getValueAsIs("USE_CHARTS"), SHOW_CORRELATION_GRAPHS: getValueAsIs("SHOW_CORRELATION_GRAPHS"), SHOW_ACTIVITY_TIME_GRAPHS: getValueAsIs("SHOW_ACTIVITY_TIME_GRAPHS"), + FILTER_HEAD_BRANCHES: !!getValueAsIs("FILTER_HEAD_BRANCHES"), + FILTER_BASE_BRANCHES: !!getValueAsIs("FILTER_BASE_BRANCHES"), }); } else { mixpanel.track("Anomymous action run", { distinct_id: "anonymous" });