From 66d416a2c50a9f5b030af133ce04abee9b553d62 Mon Sep 17 00:00:00 2001 From: coji Date: Mon, 16 Mar 2026 13:34:46 +0900 Subject: [PATCH 1/3] fix: exclude bot reviewers from Review Stacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getPendingReviewAssignments から Bot タイプのレビュアーを除外し、 Review Queue に copilot-pull-request-reviewer 等が表示されないようにした。 hasAnyReviewer サブクエリでも Bot を除外し、Bot のみアサインされた PR は Unassigned として表示されるようにした。 Co-Authored-By: Claude Opus 4.6 (1M context) --- app/routes/$orgSlug/workload/+functions/stacks.server.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/routes/$orgSlug/workload/+functions/stacks.server.ts b/app/routes/$orgSlug/workload/+functions/stacks.server.ts index ff342262..29db7af4 100644 --- a/app/routes/$orgSlug/workload/+functions/stacks.server.ts +++ b/app/routes/$orgSlug/workload/+functions/stacks.server.ts @@ -46,8 +46,11 @@ export const getOpenPullRequests = async ( .select( sql`exists( select 1 from ${sql.ref('pullRequestReviewers')} + left join ${sql.ref('companyGithubUsers')} as ${sql.ref('reviewer_user')} + on lower(${sql.ref('pullRequestReviewers.reviewer')}) = lower(${sql.ref('reviewer_user.login')}) where ${sql.ref('pullRequestReviewers.pullRequestNumber')} = ${sql.ref('pullRequests.number')} and ${sql.ref('pullRequestReviewers.repositoryId')} = ${sql.ref('pullRequests.repositoryId')} + and (${sql.ref('reviewer_user.type')} is null or ${sql.ref('reviewer_user.type')} != 'Bot') )`.as('hasAnyReviewer'), ) .execute() @@ -92,6 +95,12 @@ export const getPendingReviewAssignments = async ( .where('pullRequests.mergedAt', 'is', null) .where('pullRequests.closedAt', 'is', null) .where('pullRequestReviewers.requestedAt', 'is not', null) + .where((eb) => + eb.or([ + eb('companyGithubUsers.type', 'is', null), + eb('companyGithubUsers.type', '!=', 'Bot'), + ]), + ) .$if(teamId != null, (qb) => qb.where('repositories.teamId', '=', teamId as string), ) From 1554f5c702b56cc13724a873d974abd484635321 Mon Sep 17 00:00:00 2001 From: coji Date: Mon, 16 Mar 2026 13:38:11 +0900 Subject: [PATCH 2/3] perf: optimize hasAnyReviewer subquery to avoid per-row JOIN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LEFT JOIN in EXISTS subquery を NOT IN subselect に置き換え。 Bot ログインリストは一度だけ評価され、per-row JOIN が不要になる。 Co-Authored-By: Claude Opus 4.6 (1M context) --- app/routes/$orgSlug/workload/+functions/stacks.server.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/routes/$orgSlug/workload/+functions/stacks.server.ts b/app/routes/$orgSlug/workload/+functions/stacks.server.ts index 29db7af4..5ee2408a 100644 --- a/app/routes/$orgSlug/workload/+functions/stacks.server.ts +++ b/app/routes/$orgSlug/workload/+functions/stacks.server.ts @@ -46,11 +46,12 @@ export const getOpenPullRequests = async ( .select( sql`exists( select 1 from ${sql.ref('pullRequestReviewers')} - left join ${sql.ref('companyGithubUsers')} as ${sql.ref('reviewer_user')} - on lower(${sql.ref('pullRequestReviewers.reviewer')}) = lower(${sql.ref('reviewer_user.login')}) where ${sql.ref('pullRequestReviewers.pullRequestNumber')} = ${sql.ref('pullRequests.number')} and ${sql.ref('pullRequestReviewers.repositoryId')} = ${sql.ref('pullRequests.repositoryId')} - and (${sql.ref('reviewer_user.type')} is null or ${sql.ref('reviewer_user.type')} != 'Bot') + and lower(${sql.ref('pullRequestReviewers.reviewer')}) not in ( + select lower(${sql.ref('companyGithubUsers.login')}) from ${sql.ref('companyGithubUsers')} + where ${sql.ref('companyGithubUsers.type')} = 'Bot' + ) )`.as('hasAnyReviewer'), ) .execute() From 5c254e21c634a56ff99fa6a6fd716f70a7ddc2d1 Mon Sep 17 00:00:00 2001 From: coji Date: Mon, 16 Mar 2026 13:42:24 +0900 Subject: [PATCH 3/3] refactor: extract excludeBots helper to eliminate Bot filter duplication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 7箇所に散在していた companyGithubUsers.type != 'Bot' フィルタパターンを app/libs/tenant-query.server.ts の excludeBots ヘルパーに集約。 Co-Authored-By: Claude Opus 4.6 (1M context) --- app/libs/tenant-query.server.ts | 19 +++++++++++++++++++ .../reviews/+functions/queries.server.ts | 15 +++------------ .../deployed/+functions/queries.server.ts | 8 ++------ .../merged/+functions/queries.server.ts | 8 ++------ .../ongoing/+functions/queries.server.ts | 8 ++------ .../workload/+functions/stacks.server.ts | 15 +++------------ 6 files changed, 31 insertions(+), 42 deletions(-) create mode 100644 app/libs/tenant-query.server.ts diff --git a/app/libs/tenant-query.server.ts b/app/libs/tenant-query.server.ts new file mode 100644 index 00000000..1a96fbd2 --- /dev/null +++ b/app/libs/tenant-query.server.ts @@ -0,0 +1,19 @@ +import type { ExpressionBuilder } from 'kysely' +import type * as TenantDB from '~/app/services/tenant-type' + +/** + * companyGithubUsers.type が Bot でない行のみ残すフィルタ。 + * LEFT JOIN で companyGithubUsers に結合している前提で使う。 + * NULL(未登録ユーザー)も通す。 + */ +export function excludeBots( + eb: ExpressionBuilder< + TenantDB.DB & { companyGithubUsers: TenantDB.CompanyGithubUsers }, + keyof TenantDB.DB | 'companyGithubUsers' + >, +) { + return eb.or([ + eb('companyGithubUsers.type', 'is', null), + eb('companyGithubUsers.type', '!=', 'Bot'), + ]) +} diff --git a/app/routes/$orgSlug/analysis/reviews/+functions/queries.server.ts b/app/routes/$orgSlug/analysis/reviews/+functions/queries.server.ts index 249a706e..f1d6df31 100644 --- a/app/routes/$orgSlug/analysis/reviews/+functions/queries.server.ts +++ b/app/routes/$orgSlug/analysis/reviews/+functions/queries.server.ts @@ -1,4 +1,5 @@ import { sql } from 'kysely' +import { excludeBots } from '~/app/libs/tenant-query.server' import { getTenantDb } from '~/app/services/tenant-db.server' import type { OrganizationId } from '~/app/types/organization' @@ -122,12 +123,7 @@ export const getWipCycleRawData = async ( .$if(teamId != null, (qb) => qb.where('repositories.teamId', '=', teamId as string), ) - .where((eb) => - eb.or([ - eb('companyGithubUsers.type', 'is', null), - eb('companyGithubUsers.type', '!=', 'Bot'), - ]), - ) + .where(excludeBots) .leftJoin('pullRequestFeedbacks', (join) => join .onRef( @@ -188,12 +184,7 @@ export const getPRSizeDistribution = async ( .$if(teamId != null, (qb) => qb.where('repositories.teamId', '=', teamId as string), ) - .where((eb) => - eb.or([ - eb('companyGithubUsers.type', 'is', null), - eb('companyGithubUsers.type', '!=', 'Bot'), - ]), - ) + .where(excludeBots) .leftJoin('pullRequestFeedbacks', (join) => join .onRef( diff --git a/app/routes/$orgSlug/throughput/deployed/+functions/queries.server.ts b/app/routes/$orgSlug/throughput/deployed/+functions/queries.server.ts index 085069ae..786eed05 100644 --- a/app/routes/$orgSlug/throughput/deployed/+functions/queries.server.ts +++ b/app/routes/$orgSlug/throughput/deployed/+functions/queries.server.ts @@ -1,6 +1,7 @@ import { pipe, sortBy } from 'remeda' import { calculateBusinessHours } from '~/app/libs/business-hours' import dayjs from '~/app/libs/dayjs' +import { excludeBots } from '~/app/libs/tenant-query.server' import { getTenantDb } from '~/app/services/tenant-db.server' import type { OrganizationId } from '~/app/types/organization' @@ -31,12 +32,7 @@ export const getDeployedPullRequestReport = async ( .$if(teamId != null, (qb) => qb.where('repositories.teamId', '=', teamId as string), ) - .where((eb) => - eb.or([ - eb('companyGithubUsers.type', 'is', null), - eb('companyGithubUsers.type', '!=', 'Bot'), - ]), - ) + .where(excludeBots) .leftJoin('pullRequestFeedbacks', (join) => join .onRef( diff --git a/app/routes/$orgSlug/throughput/merged/+functions/queries.server.ts b/app/routes/$orgSlug/throughput/merged/+functions/queries.server.ts index f11485e7..5d7a50a6 100644 --- a/app/routes/$orgSlug/throughput/merged/+functions/queries.server.ts +++ b/app/routes/$orgSlug/throughput/merged/+functions/queries.server.ts @@ -1,6 +1,7 @@ import { pipe, sortBy } from 'remeda' import { calculateBusinessHours } from '~/app/libs/business-hours' import dayjs from '~/app/libs/dayjs' +import { excludeBots } from '~/app/libs/tenant-query.server' import { getTenantDb } from '~/app/services/tenant-db.server' import type { OrganizationId } from '~/app/types/organization' @@ -30,12 +31,7 @@ export const getMergedPullRequestReport = async ( .$if(teamId != null, (qb) => qb.where('repositories.teamId', '=', teamId as string), ) - .where((eb) => - eb.or([ - eb('companyGithubUsers.type', 'is', null), - eb('companyGithubUsers.type', '!=', 'Bot'), - ]), - ) + .where(excludeBots) .leftJoin('pullRequestFeedbacks', (join) => join .onRef( diff --git a/app/routes/$orgSlug/throughput/ongoing/+functions/queries.server.ts b/app/routes/$orgSlug/throughput/ongoing/+functions/queries.server.ts index 1550d6f5..b706f97a 100644 --- a/app/routes/$orgSlug/throughput/ongoing/+functions/queries.server.ts +++ b/app/routes/$orgSlug/throughput/ongoing/+functions/queries.server.ts @@ -1,6 +1,7 @@ import { pipe, sortBy } from 'remeda' import { calculateBusinessHours } from '~/app/libs/business-hours' import dayjs from '~/app/libs/dayjs' +import { excludeBots } from '~/app/libs/tenant-query.server' import { getTenantDb } from '~/app/services/tenant-db.server' import type { OrganizationId } from '~/app/types/organization' @@ -33,12 +34,7 @@ export const getOngoingPullRequestReport = async ( ) .where('mergedAt', 'is', null) .where('state', '=', 'open') - .where((eb) => - eb.or([ - eb('companyGithubUsers.type', 'is', null), - eb('companyGithubUsers.type', '!=', 'Bot'), - ]), - ) + .where(excludeBots) .leftJoin('pullRequestFeedbacks', (join) => join .onRef( diff --git a/app/routes/$orgSlug/workload/+functions/stacks.server.ts b/app/routes/$orgSlug/workload/+functions/stacks.server.ts index 5ee2408a..64071722 100644 --- a/app/routes/$orgSlug/workload/+functions/stacks.server.ts +++ b/app/routes/$orgSlug/workload/+functions/stacks.server.ts @@ -1,4 +1,5 @@ import { sql } from 'kysely' +import { excludeBots } from '~/app/libs/tenant-query.server' import { getTenantDb } from '~/app/services/tenant-db.server' import type { OrganizationId } from '~/app/types/organization' @@ -26,12 +27,7 @@ export const getOpenPullRequests = async ( .$if(teamId != null, (qb) => qb.where('repositories.teamId', '=', teamId as string), ) - .where((eb) => - eb.or([ - eb('companyGithubUsers.type', 'is', null), - eb('companyGithubUsers.type', '!=', 'Bot'), - ]), - ) + .where(excludeBots) .select([ 'pullRequests.author', 'pullRequests.number', @@ -96,12 +92,7 @@ export const getPendingReviewAssignments = async ( .where('pullRequests.mergedAt', 'is', null) .where('pullRequests.closedAt', 'is', null) .where('pullRequestReviewers.requestedAt', 'is not', null) - .where((eb) => - eb.or([ - eb('companyGithubUsers.type', 'is', null), - eb('companyGithubUsers.type', '!=', 'Bot'), - ]), - ) + .where(excludeBots) .$if(teamId != null, (qb) => qb.where('repositories.teamId', '=', teamId as string), )