Skip to content
Open
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
10 changes: 10 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ inputs:
description: "Comma-separated list of usernames to allow without write permissions, or '*' to allow all users. Only works when github_token input is provided. WARNING: Use with extreme caution - this bypasses security checks and should only be used for workflows with very limited permissions (e.g., issue labeling)."
required: false
default: ""
include_comments_by_actor:
description: "Comma-separated list of actor usernames to INCLUDE in comments. Supports wildcards: '*[bot]' matches all bots, 'dependabot[bot]' matches specific bot. Empty (default) includes all actors."
required: false
default: ""
exclude_comments_by_actor:
description: "Comma-separated list of actor usernames to EXCLUDE from comments. Supports wildcards: '*[bot]' matches all bots, 'renovate[bot]' matches specific bot. Empty (default) excludes none. If actor is in both lists, exclusion takes priority."
required: false
default: ""

# Claude Code configuration
prompt:
Expand Down Expand Up @@ -186,6 +194,8 @@ runs:
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
ALLOWED_BOTS: ${{ inputs.allowed_bots }}
ALLOWED_NON_WRITE_USERS: ${{ inputs.allowed_non_write_users }}
INCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.include_comments_by_actor }}
EXCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.exclude_comments_by_actor }}
GITHUB_RUN_ID: ${{ github.run_id }}
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
Expand Down
4 changes: 4 additions & 0 deletions src/github/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ type BaseContext = {
allowedNonWriteUsers: string;
trackProgress: boolean;
includeFixLinks: boolean;
includeCommentsByActor: string;
excludeCommentsByActor: string;
};
};

Expand Down Expand Up @@ -156,6 +158,8 @@ export function parseGitHubContext(): GitHubContext {
allowedNonWriteUsers: process.env.ALLOWED_NON_WRITE_USERS ?? "",
trackProgress: process.env.TRACK_PROGRESS === "true",
includeFixLinks: process.env.INCLUDE_FIX_LINKS === "true",
includeCommentsByActor: process.env.INCLUDE_COMMENTS_BY_ACTOR ?? "",
excludeCommentsByActor: process.env.EXCLUDE_COMMENTS_BY_ACTOR ?? "",
},
};

Expand Down
79 changes: 72 additions & 7 deletions src/github/data/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import type {
} from "../types";
import type { CommentWithImages } from "../utils/image-downloader";
import { downloadCommentImages } from "../utils/image-downloader";
import {
parseActorFilter,
shouldIncludeCommentByActor,
} from "../utils/actor-filter";

/**
* Extracts the trigger timestamp from the GitHub webhook payload.
Expand Down Expand Up @@ -166,6 +170,35 @@ export function isBodySafeToUse(
return true;
}

/**
* Filters comments by actor username based on include/exclude patterns
* @param comments - Array of comments to filter
* @param includeActors - Comma-separated actors to include
* @param excludeActors - Comma-separated actors to exclude
* @returns Filtered array of comments
*/
export function filterCommentsByActor<T extends { author: { login: string } }>(
comments: T[],
includeActors: string = "",
excludeActors: string = "",
): T[] {
const includeParsed = parseActorFilter(includeActors);
const excludeParsed = parseActorFilter(excludeActors);

// No filters = return all
if (includeParsed.length === 0 && excludeParsed.length === 0) {
return comments;
}

return comments.filter((comment) =>
shouldIncludeCommentByActor(
comment.author.login,
includeParsed,
excludeParsed,
),
);
}

type FetchDataParams = {
octokits: Octokits;
repository: string;
Expand All @@ -174,6 +207,8 @@ type FetchDataParams = {
triggerUsername?: string;
triggerTime?: string;
originalTitle?: string;
includeCommentsByActor?: string;
excludeCommentsByActor?: string;
};

export type GitHubFileWithSHA = GitHubFile & {
Expand All @@ -198,6 +233,8 @@ export async function fetchGitHubData({
triggerUsername,
triggerTime,
originalTitle,
includeCommentsByActor,
excludeCommentsByActor,
}: FetchDataParams): Promise<FetchDataResult> {
const [owner, repo] = repository.split("/");
if (!owner || !repo) {
Expand Down Expand Up @@ -225,9 +262,13 @@ export async function fetchGitHubData({
const pullRequest = prResult.repository.pullRequest;
contextData = pullRequest;
changedFiles = pullRequest.files.nodes || [];
comments = filterCommentsToTriggerTime(
pullRequest.comments?.nodes || [],
triggerTime,
comments = filterCommentsByActor(
filterCommentsToTriggerTime(
pullRequest.comments?.nodes || [],
triggerTime,
),
includeCommentsByActor,
excludeCommentsByActor,
);
reviewData = pullRequest.reviews || [];

Expand All @@ -248,9 +289,13 @@ export async function fetchGitHubData({

if (issueResult.repository.issue) {
contextData = issueResult.repository.issue;
comments = filterCommentsToTriggerTime(
contextData?.comments?.nodes || [],
triggerTime,
comments = filterCommentsByActor(
filterCommentsToTriggerTime(
contextData?.comments?.nodes || [],
triggerTime,
),
includeCommentsByActor,
excludeCommentsByActor,
);

console.log(`Successfully fetched issue #${prNumber} data`);
Expand Down Expand Up @@ -318,7 +363,27 @@ export async function fetchGitHubData({
body: r.body,
}));

// Filter review comments to trigger time
// Filter review comments to trigger time and by actor
if (reviewData && reviewData.nodes) {
// Filter reviews by actor
reviewData.nodes = filterCommentsByActor(
reviewData.nodes,
includeCommentsByActor,
excludeCommentsByActor,
);

// Also filter inline review comments within each review
reviewData.nodes.forEach((review) => {
if (review.comments?.nodes) {
review.comments.nodes = filterCommentsByActor(
review.comments.nodes,
includeCommentsByActor,
excludeCommentsByActor,
);
}
});
}

const allReviewComments =
reviewData?.nodes?.flatMap((r) => r.comments?.nodes ?? []) ?? [];
const filteredReviewComments = filterCommentsToTriggerTime(
Expand Down
65 changes: 65 additions & 0 deletions src/github/utils/actor-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* Parses actor filter string into array of patterns
* @param filterString - Comma-separated actor names (e.g., "user1,user2,*[bot]")
* @returns Array of actor patterns
*/
export function parseActorFilter(filterString: string): string[] {
if (!filterString.trim()) return [];
return filterString
.split(",")
.map((actor) => actor.trim())
.filter((actor) => actor.length > 0);
}

/**
* Checks if an actor matches a pattern
* Supports wildcards: "*[bot]" matches all bots, "dependabot[bot]" matches specific
* @param actor - Actor username to check
* @param pattern - Pattern to match against
* @returns true if actor matches pattern
*/
export function actorMatchesPattern(actor: string, pattern: string): boolean {
// Exact match
if (actor === pattern) return true;

// Wildcard bot pattern: "*[bot]" matches any username ending with [bot]
if (pattern === "*[bot]" && actor.endsWith("[bot]")) return true;

// No match
return false;
}

/**
* Determines if a comment should be included based on actor filters
* @param actor - Comment author username
* @param includeActors - Array of actors to include (empty = include all)
* @param excludeActors - Array of actors to exclude (empty = exclude none)
* @returns true if comment should be included
*/
export function shouldIncludeCommentByActor(
actor: string,
includeActors: string[],
excludeActors: string[],
): boolean {
// Check exclusion first (exclusion takes priority)
if (excludeActors.length > 0) {
for (const pattern of excludeActors) {
if (actorMatchesPattern(actor, pattern)) {
return false; // Excluded
}
}
}

// Check inclusion
if (includeActors.length > 0) {
for (const pattern of includeActors) {
if (actorMatchesPattern(actor, pattern)) {
return true; // Explicitly included
}
}
return false; // Not in include list
}

// No filters or passed all checks
return true;
}
2 changes: 2 additions & 0 deletions src/modes/tag/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export const tagMode: Mode = {
triggerUsername: context.actor,
triggerTime,
originalTitle,
includeCommentsByActor: context.inputs.includeCommentsByActor,
excludeCommentsByActor: context.inputs.excludeCommentsByActor,
});

// Setup branch
Expand Down
Loading