feat: add actor-based comment filtering to GitHub data fetching (#812)

- Introduced `include_comments_by_actor` and `exclude_comments_by_actor` inputs in action.yml to allow filtering of comments based on actor usernames.
- Updated context parsing to handle new input fields.
- Implemented `filterCommentsByActor` function to filter comments according to specified inclusion and exclusion patterns.
- Modified `fetchGitHubData` to apply actor filters when retrieving comments from pull requests and issues.
- Added comprehensive tests for the new filtering functionality.

This enhancement provides more control over which comments are processed based on the actor, improving the flexibility of the workflow.
This commit is contained in:
Rani Halabi
2026-01-27 17:48:10 +02:00
committed by GitHub
parent 231bd75b71
commit fe72061e16
11 changed files with 444 additions and 10 deletions

172
test/actor-filter.test.ts Normal file
View File

@@ -0,0 +1,172 @@
import { describe, expect, test } from "bun:test";
import {
parseActorFilter,
actorMatchesPattern,
shouldIncludeCommentByActor,
} from "../src/github/utils/actor-filter";
describe("parseActorFilter", () => {
test("parses comma-separated actors", () => {
expect(parseActorFilter("user1,user2,bot[bot]")).toEqual([
"user1",
"user2",
"bot[bot]",
]);
});
test("handles empty string", () => {
expect(parseActorFilter("")).toEqual([]);
});
test("handles whitespace-only string", () => {
expect(parseActorFilter(" ")).toEqual([]);
});
test("trims whitespace", () => {
expect(parseActorFilter(" user1 , user2 ")).toEqual(["user1", "user2"]);
});
test("filters out empty entries", () => {
expect(parseActorFilter("user1,,user2")).toEqual(["user1", "user2"]);
});
test("handles single actor", () => {
expect(parseActorFilter("user1")).toEqual(["user1"]);
});
test("handles wildcard bot pattern", () => {
expect(parseActorFilter("*[bot]")).toEqual(["*[bot]"]);
});
});
describe("actorMatchesPattern", () => {
test("matches exact username", () => {
expect(actorMatchesPattern("john-doe", "john-doe")).toBe(true);
});
test("does not match different username", () => {
expect(actorMatchesPattern("john-doe", "jane-doe")).toBe(false);
});
test("matches wildcard bot pattern", () => {
expect(actorMatchesPattern("dependabot[bot]", "*[bot]")).toBe(true);
expect(actorMatchesPattern("renovate[bot]", "*[bot]")).toBe(true);
expect(actorMatchesPattern("github-actions[bot]", "*[bot]")).toBe(true);
});
test("does not match non-bot with wildcard", () => {
expect(actorMatchesPattern("john-doe", "*[bot]")).toBe(false);
expect(actorMatchesPattern("user-bot", "*[bot]")).toBe(false);
});
test("matches specific bot", () => {
expect(actorMatchesPattern("dependabot[bot]", "dependabot[bot]")).toBe(
true,
);
expect(actorMatchesPattern("renovate[bot]", "renovate[bot]")).toBe(true);
});
test("does not match different specific bot", () => {
expect(actorMatchesPattern("dependabot[bot]", "renovate[bot]")).toBe(false);
});
test("is case sensitive", () => {
expect(actorMatchesPattern("User1", "user1")).toBe(false);
expect(actorMatchesPattern("user1", "User1")).toBe(false);
});
});
describe("shouldIncludeCommentByActor", () => {
test("includes all when no filters", () => {
expect(shouldIncludeCommentByActor("user1", [], [])).toBe(true);
expect(shouldIncludeCommentByActor("bot[bot]", [], [])).toBe(true);
});
test("excludes when in exclude list", () => {
expect(shouldIncludeCommentByActor("bot[bot]", [], ["*[bot]"])).toBe(false);
expect(shouldIncludeCommentByActor("user1", [], ["user1"])).toBe(false);
});
test("includes when not in exclude list", () => {
expect(shouldIncludeCommentByActor("user1", [], ["user2"])).toBe(true);
expect(shouldIncludeCommentByActor("user1", [], ["*[bot]"])).toBe(true);
});
test("includes when in include list", () => {
expect(shouldIncludeCommentByActor("user1", ["user1", "user2"], [])).toBe(
true,
);
expect(shouldIncludeCommentByActor("user2", ["user1", "user2"], [])).toBe(
true,
);
});
test("excludes when not in include list", () => {
expect(shouldIncludeCommentByActor("user3", ["user1", "user2"], [])).toBe(
false,
);
});
test("exclusion takes priority over inclusion", () => {
expect(shouldIncludeCommentByActor("user1", ["user1"], ["user1"])).toBe(
false,
);
expect(
shouldIncludeCommentByActor("bot[bot]", ["*[bot]"], ["*[bot]"]),
).toBe(false);
});
test("handles wildcard in include list", () => {
expect(shouldIncludeCommentByActor("dependabot[bot]", ["*[bot]"], [])).toBe(
true,
);
expect(shouldIncludeCommentByActor("renovate[bot]", ["*[bot]"], [])).toBe(
true,
);
expect(shouldIncludeCommentByActor("user1", ["*[bot]"], [])).toBe(false);
});
test("handles wildcard in exclude list", () => {
expect(shouldIncludeCommentByActor("dependabot[bot]", [], ["*[bot]"])).toBe(
false,
);
expect(shouldIncludeCommentByActor("renovate[bot]", [], ["*[bot]"])).toBe(
false,
);
expect(shouldIncludeCommentByActor("user1", [], ["*[bot]"])).toBe(true);
});
test("handles mixed include and exclude lists", () => {
// Include user1 and user2, but exclude user2
expect(
shouldIncludeCommentByActor("user1", ["user1", "user2"], ["user2"]),
).toBe(true);
expect(
shouldIncludeCommentByActor("user2", ["user1", "user2"], ["user2"]),
).toBe(false);
expect(
shouldIncludeCommentByActor("user3", ["user1", "user2"], ["user2"]),
).toBe(false);
});
test("handles complex bot filtering", () => {
// Include all bots but exclude dependabot
expect(
shouldIncludeCommentByActor(
"renovate[bot]",
["*[bot]"],
["dependabot[bot]"],
),
).toBe(true);
expect(
shouldIncludeCommentByActor(
"dependabot[bot]",
["*[bot]"],
["dependabot[bot]"],
),
).toBe(false);
expect(
shouldIncludeCommentByActor("user1", ["*[bot]"], ["dependabot[bot]"]),
).toBe(false);
});
});

View File

@@ -1,4 +1,4 @@
import { describe, expect, it, jest } from "bun:test";
import { describe, expect, it, jest, test } from "bun:test";
import {
extractTriggerTimestamp,
extractOriginalTitle,
@@ -1100,3 +1100,101 @@ describe("fetchGitHubData integration with time filtering", () => {
);
});
});
describe("filterCommentsByActor", () => {
test("filters out excluded actors", () => {
const comments = [
{ author: { login: "user1" }, body: "comment1" },
{ author: { login: "bot[bot]" }, body: "comment2" },
{ author: { login: "user2" }, body: "comment3" },
];
const { filterCommentsByActor } = require("../src/github/data/fetcher");
const filtered = filterCommentsByActor(comments, "", "*[bot]");
expect(filtered).toHaveLength(2);
expect(filtered.map((c: any) => c.author.login)).toEqual([
"user1",
"user2",
]);
});
test("includes only specified actors", () => {
const comments = [
{ author: { login: "user1" }, body: "comment1" },
{ author: { login: "user2" }, body: "comment2" },
{ author: { login: "user3" }, body: "comment3" },
];
const { filterCommentsByActor } = require("../src/github/data/fetcher");
const filtered = filterCommentsByActor(comments, "user1,user2", "");
expect(filtered).toHaveLength(2);
expect(filtered.map((c: any) => c.author.login)).toEqual([
"user1",
"user2",
]);
});
test("returns all when no filters", () => {
const comments = [
{ author: { login: "user1" }, body: "comment1" },
{ author: { login: "user2" }, body: "comment2" },
];
const { filterCommentsByActor } = require("../src/github/data/fetcher");
const filtered = filterCommentsByActor(comments, "", "");
expect(filtered).toHaveLength(2);
});
test("exclusion takes priority", () => {
const comments = [
{ author: { login: "user1" }, body: "comment1" },
{ author: { login: "user2" }, body: "comment2" },
];
const { filterCommentsByActor } = require("../src/github/data/fetcher");
const filtered = filterCommentsByActor(comments, "user1,user2", "user1");
expect(filtered).toHaveLength(1);
expect(filtered[0].author.login).toBe("user2");
});
test("filters multiple bot types", () => {
const comments = [
{ author: { login: "user1" }, body: "comment1" },
{ author: { login: "dependabot[bot]" }, body: "comment2" },
{ author: { login: "renovate[bot]" }, body: "comment3" },
{ author: { login: "user2" }, body: "comment4" },
];
const { filterCommentsByActor } = require("../src/github/data/fetcher");
const filtered = filterCommentsByActor(comments, "", "*[bot]");
expect(filtered).toHaveLength(2);
expect(filtered.map((c: any) => c.author.login)).toEqual([
"user1",
"user2",
]);
});
test("filters specific bot only", () => {
const comments = [
{ author: { login: "dependabot[bot]" }, body: "comment1" },
{ author: { login: "renovate[bot]" }, body: "comment2" },
{ author: { login: "user1" }, body: "comment3" },
];
const { filterCommentsByActor } = require("../src/github/data/fetcher");
const filtered = filterCommentsByActor(comments, "", "dependabot[bot]");
expect(filtered).toHaveLength(2);
expect(filtered.map((c: any) => c.author.login)).toEqual([
"renovate[bot]",
"user1",
]);
});
test("handles empty comment array", () => {
const comments: any[] = [];
const { filterCommentsByActor } = require("../src/github/data/fetcher");
const filtered = filterCommentsByActor(comments, "user1", "");
expect(filtered).toHaveLength(0);
});
});

View File

@@ -39,6 +39,8 @@ describe("prepareMcpConfig", () => {
allowedNonWriteUsers: "",
trackProgress: false,
includeFixLinks: true,
includeCommentsByActor: "",
excludeCommentsByActor: "",
},
};

View File

@@ -27,6 +27,8 @@ const defaultInputs = {
allowedNonWriteUsers: "",
trackProgress: false,
includeFixLinks: true,
includeCommentsByActor: "",
excludeCommentsByActor: "",
};
const defaultRepository = {
@@ -55,7 +57,12 @@ export const createMockContext = (
};
const mergedInputs = overrides.inputs
? { ...defaultInputs, ...overrides.inputs }
? {
...defaultInputs,
...overrides.inputs,
includeCommentsByActor: overrides.inputs.includeCommentsByActor ?? "",
excludeCommentsByActor: overrides.inputs.excludeCommentsByActor ?? "",
}
: defaultInputs;
return { ...baseContext, ...overrides, inputs: mergedInputs };
@@ -79,7 +86,12 @@ export const createMockAutomationContext = (
};
const mergedInputs = overrides.inputs
? { ...defaultInputs, ...overrides.inputs }
? {
...defaultInputs,
...overrides.inputs,
includeCommentsByActor: overrides.inputs.includeCommentsByActor ?? "",
excludeCommentsByActor: overrides.inputs.excludeCommentsByActor ?? "",
}
: { ...defaultInputs };
return { ...baseContext, ...overrides, inputs: mergedInputs };

View File

@@ -27,6 +27,8 @@ describe("detectMode with enhanced routing", () => {
allowedNonWriteUsers: "",
trackProgress: false,
includeFixLinks: true,
includeCommentsByActor: "",
excludeCommentsByActor: "",
},
};

View File

@@ -75,6 +75,8 @@ describe("checkWritePermissions", () => {
allowedNonWriteUsers: "",
trackProgress: false,
includeFixLinks: true,
includeCommentsByActor: "",
excludeCommentsByActor: "",
},
});