diff --git a/app/routes/$orgSlug/settings/_index/+forms/organization-settings.tsx b/app/routes/$orgSlug/settings/_index/+forms/organization-settings.tsx
index ee991f56..fefd7910 100644
--- a/app/routes/$orgSlug/settings/_index/+forms/organization-settings.tsx
+++ b/app/routes/$orgSlug/settings/_index/+forms/organization-settings.tsx
@@ -40,7 +40,6 @@ export const OrganizationSettings = ({
releaseDetectionMethod: string
releaseDetectionKey: string
isActive: number
- excludedUsers: string
timezone: string
language: string
}
@@ -60,7 +59,6 @@ export const OrganizationSettings = ({
releaseDetectionMethod: organizationSetting?.releaseDetectionMethod,
releaseDetectionKey: organizationSetting?.releaseDetectionKey,
isActive: organizationSetting?.isActive ? '1' : undefined,
- excludedUsers: organizationSetting?.excludedUsers,
timezone: organizationSetting?.timezone ?? DEFAULT_TIMEZONE,
language: organizationSetting?.language,
},
@@ -173,21 +171,6 @@ export const OrganizationSettings = ({
{fields.isActive.errors}
-
-
{form.errors && (
System Error
diff --git a/app/routes/$orgSlug/settings/_index/+functions/mutations.server.ts b/app/routes/$orgSlug/settings/_index/+functions/mutations.server.ts
index fad98b39..d2a491b4 100644
--- a/app/routes/$orgSlug/settings/_index/+functions/mutations.server.ts
+++ b/app/routes/$orgSlug/settings/_index/+functions/mutations.server.ts
@@ -26,7 +26,6 @@ export const updateOrganizationSetting = async (
| 'releaseDetectionMethod'
| 'releaseDetectionKey'
| 'isActive'
- | 'excludedUsers'
| 'timezone'
| 'language'
>,
diff --git a/app/routes/$orgSlug/settings/_index/+schema.ts b/app/routes/$orgSlug/settings/_index/+schema.ts
index d5851804..cba368e4 100644
--- a/app/routes/$orgSlug/settings/_index/+schema.ts
+++ b/app/routes/$orgSlug/settings/_index/+schema.ts
@@ -11,7 +11,6 @@ export const organizationSettingsSchema = z.object({
.literal('on')
.optional()
.transform((val) => (val === 'on' ? 1 : 0)),
- excludedUsers: z.string().max(2000).default(''),
timezone: z.string().refine((v) => VALID_TIMEZONES.has(v), {
message: 'Invalid timezone',
}),
diff --git a/app/routes/$orgSlug/settings/_index/index.tsx b/app/routes/$orgSlug/settings/_index/index.tsx
index f5a5cb56..e75da0e5 100644
--- a/app/routes/$orgSlug/settings/_index/index.tsx
+++ b/app/routes/$orgSlug/settings/_index/index.tsx
@@ -44,7 +44,6 @@ export const action = async ({ request, context }: Route.ActionArgs) => {
releaseDetectionMethod,
releaseDetectionKey,
isActive,
- excludedUsers,
timezone,
language,
} = submission.value
@@ -55,7 +54,6 @@ export const action = async ({ request, context }: Route.ActionArgs) => {
releaseDetectionMethod,
releaseDetectionKey,
isActive,
- excludedUsers,
timezone,
language,
})
diff --git a/app/routes/$orgSlug/settings/github-users._index/+components/github-users-columns.tsx b/app/routes/$orgSlug/settings/github-users._index/+components/github-users-columns.tsx
index f96cdff9..9c45ae78 100644
--- a/app/routes/$orgSlug/settings/github-users._index/+components/github-users-columns.tsx
+++ b/app/routes/$orgSlug/settings/github-users._index/+components/github-users-columns.tsx
@@ -149,18 +149,18 @@ export function createColumns(timezone: string): ColumnDef[] {
),
},
{
- accessorKey: 'createdAt',
+ accessorKey: 'lastActivityAt',
header: ({ column }) => (
-
- ),
- cell: ({ row }) => (
-
- {dayjs
- .utc(row.getValue('createdAt'))
- .tz(timezone)
- .format('YYYY-MM-DD')}
-
+
),
+ cell: ({ row }) => {
+ const value = row.getValue('lastActivityAt')
+ return (
+
+ {value ? dayjs.utc(value).tz(timezone).fromNow() : '-'}
+
+ )
+ },
},
{
id: 'actions',
diff --git a/app/routes/$orgSlug/settings/github-users._index/+hooks/use-data-table-state.ts b/app/routes/$orgSlug/settings/github-users._index/+hooks/use-data-table-state.ts
index b1009406..f4f2a05e 100644
--- a/app/routes/$orgSlug/settings/github-users._index/+hooks/use-data-table-state.ts
+++ b/app/routes/$orgSlug/settings/github-users._index/+hooks/use-data-table-state.ts
@@ -27,7 +27,7 @@ export const SortSchema = z.object({
z
.union([z.literal('asc'), z.literal('desc')])
.optional()
- .default('asc'),
+ .default('desc'),
),
})
@@ -99,7 +99,7 @@ export function useDataTableState() {
(prev) => {
if (newSort.sort_by) {
prev.set('sort_by', newSort.sort_by)
- prev.set('sort_order', newSort.sort_order || 'asc')
+ prev.set('sort_order', newSort.sort_order || 'desc')
} else {
prev.delete('sort_by')
prev.delete('sort_order')
diff --git a/app/routes/$orgSlug/settings/github-users._index/queries.server.ts b/app/routes/$orgSlug/settings/github-users._index/queries.server.ts
index 733bdf86..4109d749 100644
--- a/app/routes/$orgSlug/settings/github-users._index/queries.server.ts
+++ b/app/routes/$orgSlug/settings/github-users._index/queries.server.ts
@@ -31,6 +31,7 @@ export const listFilteredGithubUsers = async ({
'companyGithubUsers.displayName',
'companyGithubUsers.type',
'companyGithubUsers.isActive',
+ 'companyGithubUsers.lastActivityAt',
'companyGithubUsers.createdAt',
])
@@ -71,12 +72,22 @@ export const listFilteredGithubUsers = async ({
displayName: 'companyGithubUsers.displayName',
type: 'companyGithubUsers.type',
isActive: 'companyGithubUsers.isActive',
+ lastActivityAt: 'companyGithubUsers.lastActivityAt',
createdAt: 'companyGithubUsers.createdAt',
}
- const safeSortBy = sortFieldMap[sortBy ?? ''] ?? sortFieldMap.login
+ const safeSortBy = sortFieldMap[sortBy ?? ''] ?? sortFieldMap.lastActivityAt
+
+ // NULL を末尾に配置(lastActivityAt など nullable カラムのソート用)
+ let sortedQuery = query
+ if (safeSortBy === sortFieldMap.lastActivityAt) {
+ sortedQuery = sortedQuery.orderBy(
+ sql`${sql.ref(safeSortBy)} IS NULL`,
+ sortOrder === 'desc' ? 'asc' : 'desc',
+ )
+ }
const [rows, countResult] = await Promise.all([
- query
+ sortedQuery
.orderBy(sql.ref(safeSortBy), sortOrder)
.limit(pageSize)
.offset((currentPage - 1) * pageSize)
diff --git a/app/routes/$orgSlug/settings/repositories/$repository/$pull/index.tsx b/app/routes/$orgSlug/settings/repositories/$repository/$pull/index.tsx
index 7acad11d..9f593162 100644
--- a/app/routes/$orgSlug/settings/repositories/$repository/$pull/index.tsx
+++ b/app/routes/$orgSlug/settings/repositories/$repository/$pull/index.tsx
@@ -11,6 +11,7 @@ import {
upsertPullRequestReview,
upsertPullRequestReviewers,
} from '~/batch/db/mutations'
+import { getBotLogins } from '~/batch/db/queries'
import { createFetcher } from '~/batch/github/fetcher'
import type { ShapedGitHubPullRequest } from '~/batch/github/model'
import { buildPullRequests } from '~/batch/github/pullrequest'
@@ -161,15 +162,18 @@ export const action = async ({
timelineItems,
})
- // 4. Get organization settings for build config
- const settings = await getOrganizationSettings(organization.id)
+ // 4. Get organization settings and bot logins for build config
+ const [settings, botLoginsList] = await Promise.all([
+ getOrganizationSettings(organization.id),
+ getBotLogins(organization.id),
+ ])
// 5. Build pull request data (analyze)
const result = await buildPullRequests(
{
organizationId: organization.id,
repositoryId,
- excludedUsers: settings?.excludedUsers ?? '',
+ botLogins: new Set(botLoginsList),
releaseDetectionMethod: settings?.releaseDetectionMethod ?? 'branch',
releaseDetectionKey: settings?.releaseDetectionKey ?? '',
},
diff --git a/app/routes/$orgSlug/settings/repositories/$repository/$pull/queries.server.ts b/app/routes/$orgSlug/settings/repositories/$repository/$pull/queries.server.ts
index 87569087..78747c6b 100644
--- a/app/routes/$orgSlug/settings/repositories/$repository/$pull/queries.server.ts
+++ b/app/routes/$orgSlug/settings/repositories/$repository/$pull/queries.server.ts
@@ -59,7 +59,7 @@ export const getOrganizationSettings = async (
const tenantDb = getTenantDb(organizationId)
return await tenantDb
.selectFrom('organizationSettings')
- .select(['releaseDetectionMethod', 'releaseDetectionKey', 'excludedUsers'])
+ .select(['releaseDetectionMethod', 'releaseDetectionKey'])
.executeTakeFirst()
}
diff --git a/app/services/jobs/analyze-worker.ts b/app/services/jobs/analyze-worker.ts
index 60e2cd5e..dc4488b2 100644
--- a/app/services/jobs/analyze-worker.ts
+++ b/app/services/jobs/analyze-worker.ts
@@ -17,7 +17,7 @@ interface WorkerInput {
repositoryId: string
releaseDetectionMethod: string
releaseDetectionKey: string
- excludedUsers: string
+ botLogins: string[]
filterPrNumbers?: number[]
}
@@ -47,11 +47,14 @@ const result: Awaited> =
repositoryId: input.repositoryId,
releaseDetectionMethod: input.releaseDetectionMethod,
releaseDetectionKey: input.releaseDetectionKey,
- excludedUsers: input.excludedUsers,
+ botLogins: new Set(input.botLogins),
},
await store.loader.pullrequests(),
store.loader,
input.filterPrNumbers ? new Set(input.filterPrNumbers) : undefined,
)
-parentPort?.postMessage(result)
+parentPort?.postMessage({
+ ...result,
+ botUsers: [...result.botUsers],
+})
diff --git a/app/services/jobs/crawl.server.ts b/app/services/jobs/crawl.server.ts
index 993a7857..8284342f 100644
--- a/app/services/jobs/crawl.server.ts
+++ b/app/services/jobs/crawl.server.ts
@@ -34,6 +34,7 @@ export const crawlJob = defineJob({
}
return {
organizationSetting: org.organizationSetting,
+ botLogins: org.botLogins,
repositories: org.repositories,
exportSetting: org.exportSetting,
}
diff --git a/app/services/jobs/recalculate.server.ts b/app/services/jobs/recalculate.server.ts
index d0d11478..1cface71 100644
--- a/app/services/jobs/recalculate.server.ts
+++ b/app/services/jobs/recalculate.server.ts
@@ -31,6 +31,7 @@ export const recalculateJob = defineJob({
}
return {
organizationSetting: org.organizationSetting,
+ botLogins: org.botLogins,
repositories: org.repositories,
exportSetting: org.exportSetting,
}
diff --git a/app/services/jobs/run-in-worker.ts b/app/services/jobs/run-in-worker.ts
index a4c46388..6d9559ac 100644
--- a/app/services/jobs/run-in-worker.ts
+++ b/app/services/jobs/run-in-worker.ts
@@ -7,7 +7,7 @@ interface AnalyzeWorkerInput {
repositoryId: string
releaseDetectionMethod: string
releaseDetectionKey: string
- excludedUsers: string
+ botLogins: string[]
filterPrNumbers?: number[]
}
diff --git a/app/services/jobs/shared-steps.server.ts b/app/services/jobs/shared-steps.server.ts
index 5f687dc2..6f21e452 100644
--- a/app/services/jobs/shared-steps.server.ts
+++ b/app/services/jobs/shared-steps.server.ts
@@ -25,13 +25,15 @@ interface AnalyzeResult {
reviews: AnalyzedReview[]
reviewers: AnalyzedReviewer[]
reviewResponses: AnalyzedReviewResponse[]
+ botUsers: string[]
}
interface OrganizationData {
organizationSetting: Pick<
Selectable,
- 'releaseDetectionMethod' | 'releaseDetectionKey' | 'excludedUsers'
+ 'releaseDetectionMethod' | 'releaseDetectionKey'
>
+ botLogins: string[]
repositories: Selectable[]
exportSetting?: Selectable | null
}
@@ -107,6 +109,7 @@ export async function analyzeAndFinalizeSteps(
const allReviews: AnalyzedReview[] = []
const allReviewers: AnalyzedReviewer[] = []
const allReviewResponses: AnalyzedReviewResponse[] = []
+ const allBotUsers = new Set()
const sqliteBusyEvents: SqliteBusyEvent[] = []
for (let i = 0; i < organization.repositories.length; i++) {
@@ -127,7 +130,7 @@ export async function analyzeAndFinalizeSteps(
repo.releaseDetectionMethod ?? orgSetting.releaseDetectionMethod,
releaseDetectionKey:
repo.releaseDetectionKey ?? orgSetting.releaseDetectionKey,
- excludedUsers: orgSetting.excludedUsers,
+ botLogins: organization.botLogins,
filterPrNumbers: prNumbers ? [...prNumbers] : undefined,
},
{
@@ -140,6 +143,7 @@ export async function analyzeAndFinalizeSteps(
allReviews.push(...result.reviews)
allReviewers.push(...result.reviewers)
allReviewResponses.push(...result.reviewResponses)
+ for (const login of result.botUsers) allBotUsers.add(login)
}
// Upsert
@@ -151,6 +155,7 @@ export async function analyzeAndFinalizeSteps(
pulls: allPulls,
reviews: allReviews,
reviewers: allReviewers,
+ botUsers: allBotUsers,
})
})
})
diff --git a/app/services/tenant-type.ts b/app/services/tenant-type.ts
index 4f072942..7baace66 100644
--- a/app/services/tenant-type.ts
+++ b/app/services/tenant-type.ts
@@ -28,6 +28,7 @@ export interface CompanyGithubUsers {
createdAt: Generated;
displayName: string;
isActive: 0 | 1;
+ lastActivityAt: string | null;
login: string;
type: string | null;
updatedAt: string;
@@ -70,7 +71,6 @@ export interface Integrations {
export interface OrganizationSettings {
createdAt: Generated;
- excludedUsers: Generated;
id: string;
isActive: Generated<0 | 1>;
language: Generated<"en" | "ja">;
diff --git a/batch/db/mutations.ts b/batch/db/mutations.ts
index b3acd3e6..09f848e8 100644
--- a/batch/db/mutations.ts
+++ b/batch/db/mutations.ts
@@ -225,6 +225,7 @@ export async function batchReplacePullRequestReviewers(
export async function upsertCompanyGithubUsers(
organizationId: OrganizationId,
logins: string[],
+ botUsers?: Set,
) {
if (logins.length === 0) return
@@ -240,11 +241,20 @@ export async function upsertCompanyGithubUsers(
uniqueLogins.map((login) => ({
login,
displayName: login,
+ type: botUsers?.has(login) ? 'Bot' : null,
isActive: 0,
updatedAt: now,
})),
)
- .onConflict((oc) => oc.column('login').doNothing())
+ .onConflict((oc) =>
+ oc.column('login').doUpdateSet((eb) => ({
+ // API で bot と判定されたユーザーの type を自動設定(未設定の場合のみ)
+ type: eb.fn.coalesce(
+ eb.ref('companyGithubUsers.type'),
+ eb.ref('excluded.type'),
+ ),
+ })),
+ )
.execute()
logger.info(
`upserted ${uniqueLogins.length} company github users.`,
@@ -252,6 +262,40 @@ export async function upsertCompanyGithubUsers(
)
}
+function trackLatest(map: Map, login: string, ts: string) {
+ const key = login.toLowerCase()
+ const current = map.get(key)
+ if (!current || ts > current) {
+ map.set(key, ts)
+ }
+}
+
+/**
+ * ユーザーごとの最終活動日時を更新する。
+ * 既存値より新しい場合のみ上書き。
+ */
+async function updateLastActivityAt(
+ organizationId: OrganizationId,
+ lastActivity: Map,
+) {
+ if (lastActivity.size === 0) return
+ const tenantDb = getTenantDb(organizationId)
+
+ for (const [login, ts] of lastActivity) {
+ await tenantDb
+ .updateTable('companyGithubUsers')
+ .set({ lastActivityAt: ts })
+ .where('login', '=', login)
+ .where((eb) =>
+ eb.or([
+ eb('lastActivityAt', 'is', null),
+ eb('lastActivityAt', '<', ts),
+ ]),
+ )
+ .execute()
+ }
+}
+
/**
* analyze 結果を一括で DB に書き込む共通関数。
* durably ジョブ(crawl, recalculate)の共通 upsert 処理。
@@ -262,6 +306,7 @@ export async function upsertAnalyzedData(
pulls: Selectable[]
reviews: AnalyzedReview[]
reviewers: AnalyzedReviewer[]
+ botUsers?: Set
},
) {
// Auto-register discovered GitHub users
@@ -277,7 +322,22 @@ export async function upsertAnalyzedData(
if (r.login) discoveredLogins.add(r.login)
}
}
- await upsertCompanyGithubUsers(organizationId, [...discoveredLogins])
+ await upsertCompanyGithubUsers(
+ organizationId,
+ [...discoveredLogins],
+ data.botUsers,
+ )
+
+ // Update last activity timestamps
+ const lastActivity = new Map()
+ for (const pr of data.pulls) {
+ if (pr.author) trackLatest(lastActivity, pr.author, pr.pullRequestCreatedAt)
+ }
+ for (const review of data.reviews) {
+ if (review.reviewer)
+ trackLatest(lastActivity, review.reviewer, review.submittedAt)
+ }
+ await updateLastActivityAt(organizationId, lastActivity)
// Upsert pull requests
logger.info('upsert started...', organizationId)
diff --git a/batch/db/queries.ts b/batch/db/queries.ts
index b0452661..88da5948 100644
--- a/batch/db/queries.ts
+++ b/batch/db/queries.ts
@@ -18,12 +18,7 @@ async function getTenantData(organizationId: OrganizationId) {
await Promise.all([
tenantDb
.selectFrom('organizationSettings')
- .select([
- 'releaseDetectionMethod',
- 'releaseDetectionKey',
- 'isActive',
- 'excludedUsers',
- ])
+ .select(['releaseDetectionMethod', 'releaseDetectionKey', 'isActive'])
.executeTakeFirst(),
tenantDb
.selectFrom('integrations')
@@ -64,6 +59,18 @@ async function getTenantData(organizationId: OrganizationId) {
}
}
+export async function getBotLogins(
+ organizationId: OrganizationId,
+): Promise {
+ const tenantDb = getTenantDb(organizationId)
+ const rows = await tenantDb
+ .selectFrom('companyGithubUsers')
+ .select('login')
+ .where('type', '=', 'Bot')
+ .execute()
+ return rows.map((r) => r.login.toLowerCase())
+}
+
export const listAllOrganizations = async () => {
const orgs = await db
.selectFrom('organizations')
@@ -85,6 +92,9 @@ export const getOrganization = async (organizationId: OrganizationId) => {
.where('id', '=', organizationId)
.executeTakeFirstOrThrow()
- const tenantData = await getTenantData(org.id as OrganizationId)
- return { ...org, ...tenantData }
+ const [tenantData, botLogins] = await Promise.all([
+ getTenantData(org.id as OrganizationId),
+ getBotLogins(org.id as OrganizationId),
+ ])
+ return { ...org, ...tenantData, botLogins }
}
diff --git a/batch/github/__tests__/buildPullRequests-filter.test.ts b/batch/github/__tests__/buildPullRequests-filter.test.ts
index 358ce490..e67a0b9e 100644
--- a/batch/github/__tests__/buildPullRequests-filter.test.ts
+++ b/batch/github/__tests__/buildPullRequests-filter.test.ts
@@ -22,6 +22,7 @@ const basePr: ShapedGitHubPullRequest = {
body: null,
url: 'https://github.com/test-org/test-repo/pull/0',
author: 'author1',
+ authorIsBot: false,
assignees: [],
reviewers: [],
draft: false,
@@ -206,7 +207,7 @@ const config = {
repositoryId: 'repo-1',
releaseDetectionMethod: 'branch',
releaseDetectionKey: 'main',
- excludedUsers: '',
+ botLogins: new Set(),
}
// --- テスト ---
diff --git a/batch/github/__tests__/release-detect.test.ts b/batch/github/__tests__/release-detect.test.ts
index 86852111..6cfdd855 100644
--- a/batch/github/__tests__/release-detect.test.ts
+++ b/batch/github/__tests__/release-detect.test.ts
@@ -16,6 +16,7 @@ const basePr: ShapedGitHubPullRequest = {
body: null,
url: '',
author: 'author1',
+ authorIsBot: false,
assignees: [],
reviewers: [],
draft: false,
diff --git a/batch/github/fetcher.ts b/batch/github/fetcher.ts
index eeb3e13f..f1547094 100644
--- a/batch/github/fetcher.ts
+++ b/batch/github/fetcher.ts
@@ -52,6 +52,7 @@ const GetPullRequestsQuery = graphql(`
oid
}
author {
+ __typename
login
}
assignees(first: 100) {
@@ -350,6 +351,7 @@ const GetPullRequestsWithDetailsQuery = graphql(`
oid
}
author {
+ __typename
login
}
assignees(first: 10) {
@@ -869,6 +871,7 @@ export const createFetcher = ({ owner, repo, token }: createFetcherProps) => {
body: node.body ?? null,
url: node.url,
author: node.author?.login ?? null,
+ authorIsBot: node.author?.__typename === 'Bot',
assignees:
node.assignees.nodes
?.filter((n) => n != null)
@@ -1190,6 +1193,7 @@ export const createFetcher = ({ owner, repo, token }: createFetcherProps) => {
body: node.body ?? null,
url: node.url,
author: node.author?.login ?? null,
+ authorIsBot: node.author?.__typename === 'Bot',
assignees:
node.assignees.nodes
?.filter((n) => n != null)
diff --git a/batch/github/model.ts b/batch/github/model.ts
index f3cf1e5c..268cd77f 100644
--- a/batch/github/model.ts
+++ b/batch/github/model.ts
@@ -20,6 +20,7 @@ export type ShapedGitHubPullRequest = {
body: string | null
url: GitHubPullRequest['html_url']
author: NonNullable['login'] | null
+ authorIsBot: boolean
assignees: string[]
reviewers: { login: string; requestedAt: string | null }[]
draft: boolean
diff --git a/batch/github/pullrequest.ts b/batch/github/pullrequest.ts
index 5a07a6e4..82532386 100644
--- a/batch/github/pullrequest.ts
+++ b/batch/github/pullrequest.ts
@@ -31,9 +31,6 @@ import type {
PullRequestLoaders,
} from './types'
-// デフォルトで除外するユーザー (GitHub API で [bot] 接尾辞なしで記録されるbot)
-const DEFAULT_EXCLUDED_USERS = ['Copilot']
-
/** PR に関連するアーティファクトの型 */
interface PrArtifacts {
commits: ShapedGitHubCommit[]
@@ -55,7 +52,7 @@ interface BuildConfig {
repositoryId: string
releaseDetectionMethod: string
releaseDetectionKey: string
- excludedUsers: string
+ botLogins: Set
}
const nullOrDate = (dateStr?: Date | string | null) => {
@@ -82,7 +79,7 @@ async function loadPrArtifacts(
function filterActors(
artifacts: PrArtifacts,
pr: ShapedGitHubPullRequest,
- excludedUsers: string[],
+ botLogins: Set,
): PrArtifacts {
return {
commits: artifacts.commits,
@@ -90,13 +87,13 @@ function filterActors(
(r) =>
!r.isBot &&
r.user !== pr.author &&
- !excludedUsers.includes(r.user ?? ''),
+ !botLogins.has((r.user ?? '').toLowerCase()),
),
discussions: artifacts.discussions.filter(
(d) =>
!d.isBot &&
d.user !== pr.author &&
- !excludedUsers.includes(d.user ?? ''),
+ !botLogins.has((d.user ?? '').toLowerCase()),
),
}
}
@@ -188,13 +185,6 @@ export const buildPullRequests = async (
loaders: PullRequestLoaders,
filterPrNumbers?: Set,
) => {
- // カンマ区切りの除外ユーザーリストをパース
- const customExcludedUsers = config.excludedUsers
- .split(',')
- .map((u) => u.trim())
- .filter((u) => u.length > 0)
- const excludedUsers = [...DEFAULT_EXCLUDED_USERS, ...customExcludedUsers]
-
// リリース日ルックアップを事前構築
// 注: filterPrNumbers に関係なく全 PR から構築する(リリースPR自体がフィルタ外でも必要)
let branchReleaseLookup: Map | null = null
@@ -216,6 +206,7 @@ export const buildPullRequests = async (
const reviews: AnalyzedReview[] = []
const reviewers: AnalyzedReviewer[] = []
const reviewResponses: AnalyzedReviewResponse[] = []
+ const botUsers = new Set()
let processed = 0
for (const pr of pullrequests) {
@@ -233,10 +224,19 @@ export const buildPullRequests = async (
// 1. アーティファクト読み込み(I/O)
const rawArtifacts = await loadPrArtifacts(pr, loaders)
- // 2. アクター除外フィルタ(純粋関数)
- const artifacts = filterActors(rawArtifacts, pr, excludedUsers)
+ // 2. bot ユーザーを収集(GitHub API の __typename === 'Bot')
+ if (pr.authorIsBot && pr.author) botUsers.add(pr.author.toLowerCase())
+ for (const r of rawArtifacts.reviews) {
+ if (r.isBot && r.user) botUsers.add(r.user.toLowerCase())
+ }
+ for (const d of rawArtifacts.discussions) {
+ if (d.isBot && d.user) botUsers.add(d.user.toLowerCase())
+ }
+
+ // 3. アクター除外フィルタ(純粋関数)
+ const artifacts = filterActors(rawArtifacts, pr, config.botLogins)
- // 3. レビューレスポンス解析
+ // 4. レビューレスポンス解析
reviewResponses.push(
...analyzeReviewResponse(artifacts.discussions).map((res) => ({
repo: String(pr.repo),
@@ -247,10 +247,10 @@ export const buildPullRequests = async (
})),
)
- // 4. 日時計算(純粋関数)
+ // 5. 日時計算(純粋関数)
const dates = computeDates(pr, artifacts)
- // 5. リリース日時計算(事前計算済みルックアップから O(1) or O(log n) で取得)
+ // 6. リリース日時計算(事前計算済みルックアップから O(1) or O(log n) で取得)
let releasedAt: string | null = null
if (pr.mergedAt) {
if (branchReleaseLookup) {
@@ -260,12 +260,12 @@ export const buildPullRequests = async (
}
}
- // 6. PR 行データ生成(純粋関数)
+ // 7. PR 行データ生成(純粋関数)
pulls.push(
buildPullRequestRow(pr, dates, releasedAt, config.repositoryId),
)
- // 7. レビュー情報を収集(PENDING レビューは submittedAt がないため除外)
+ // 8. レビュー情報を収集(PENDING レビューは submittedAt がないため除外)
for (const review of rawArtifacts.reviews) {
if (!review.user || !review.submittedAt || review.state === 'PENDING')
continue
@@ -280,7 +280,7 @@ export const buildPullRequests = async (
})
}
- // 8. レビュアー(レビュー依頼先)情報を収集
+ // 9. レビュアー(レビュー依頼先)情報を収集
// timeline_items から requestedAt を補完する
// reviewer が 0 人でも push して、removed された reviewer の DB レコードを削除させる
const prReviewers = pr.reviewers ?? []
@@ -307,5 +307,5 @@ export const buildPullRequests = async (
}
}
- return { pulls, reviews, reviewers, reviewResponses }
+ return { pulls, reviews, reviewers, reviewResponses, botUsers }
}
diff --git a/batch/github/store.test.ts b/batch/github/store.test.ts
index 872cd830..a1ddfff1 100644
--- a/batch/github/store.test.ts
+++ b/batch/github/store.test.ts
@@ -87,6 +87,7 @@ const makePr = (number: number): ShapedGitHubPullRequest => ({
body: null,
url: `https://github.com/test-owner/test-repo/pull/${number}`,
author: 'user1',
+ authorIsBot: false,
assignees: [],
reviewers: [{ login: 'reviewer1', requestedAt: '2024-01-01T06:00:00Z' }],
draft: false,
diff --git a/db/migrations/tenant/20260318120000_migrate_excluded_users_to_bot_type.sql b/db/migrations/tenant/20260318120000_migrate_excluded_users_to_bot_type.sql
new file mode 100644
index 00000000..18a3c5f7
--- /dev/null
+++ b/db/migrations/tenant/20260318120000_migrate_excluded_users_to_bot_type.sql
@@ -0,0 +1,21 @@
+-- Migrate excludedUsers setting to companyGithubUsers.type='Bot'
+-- 1. Copy excluded users from organization_settings to company_github_users with type='Bot'
+INSERT INTO company_github_users (login, display_name, type, is_active, updated_at)
+SELECT
+ LOWER(TRIM(value)) AS login,
+ LOWER(TRIM(value)) AS display_name,
+ 'Bot' AS type,
+ 0 AS is_active,
+ CURRENT_TIMESTAMP AS updated_at
+FROM organization_settings, json_each('["' || REPLACE(COALESCE(excluded_users, ''), ',', '","') || '"]')
+WHERE COALESCE(excluded_users, '') != ''
+ AND TRIM(value) != ''
+ON CONFLICT (login) DO UPDATE SET type = 'Bot';
+
+-- 2. Ensure 'copilot' is registered as Bot (case-insensitive - stored lowercase)
+INSERT INTO company_github_users (login, display_name, type, is_active, updated_at)
+VALUES ('copilot', 'copilot', 'Bot', 0, CURRENT_TIMESTAMP)
+ON CONFLICT (login) DO UPDATE SET type = 'Bot';
+
+-- 3. Drop excluded_users column from organization_settings
+ALTER TABLE organization_settings DROP COLUMN excluded_users;
diff --git a/db/migrations/tenant/20260319120000_add_last_activity_at.sql b/db/migrations/tenant/20260319120000_add_last_activity_at.sql
new file mode 100644
index 00000000..d7d45615
--- /dev/null
+++ b/db/migrations/tenant/20260319120000_add_last_activity_at.sql
@@ -0,0 +1,13 @@
+-- Add last_activity_at column to company_github_users
+ALTER TABLE company_github_users ADD COLUMN last_activity_at text NULL;
+
+-- Backfill from existing PR and review data
+UPDATE company_github_users SET last_activity_at = (
+ SELECT MAX(activity_at) FROM (
+ SELECT MAX(pull_request_created_at) AS activity_at
+ FROM pull_requests WHERE LOWER(author) = LOWER(company_github_users.login)
+ UNION ALL
+ SELECT MAX(submitted_at) AS activity_at
+ FROM pull_request_reviews WHERE LOWER(reviewer) = LOWER(company_github_users.login)
+ )
+);
diff --git a/db/migrations/tenant/atlas.sum b/db/migrations/tenant/atlas.sum
index 6d774d25..dcb63a67 100644
--- a/db/migrations/tenant/atlas.sum
+++ b/db/migrations/tenant/atlas.sum
@@ -1,4 +1,4 @@
-h1:57BFTZmY6MHNsyxYn2I1qlUPbtDkdrAtNqeUx2hu+Sg=
+h1:WXEFgffupsvOjsoZyUVrgYzOVvKHu7gpaFCM4HJS/Lk=
20260226112249_initial_tenant.sql h1:dIhBg2gzyh+ZjLzPXdHYafd5e62yIEjk1eFlllEyYX0=
20260226233619_add_teams.sql h1:n8MRMUA4BgeXYEnL9HJPc8mnXh8lqIfrCcdYtFFoWqw=
20260227163239.sql h1:ENMZUW7zHK8UjG2TdYlBOZSVPPUCXftIw5U5k2C54oo=
@@ -18,3 +18,5 @@ h1:57BFTZmY6MHNsyxYn2I1qlUPbtDkdrAtNqeUx2hu+Sg=
20260315050936.sql h1:/o/ku2qrlT14mxxhETs26eHguobD5wPYES2khLSN2wA=
20260315120000_add_language.sql h1:O1oFQ+aUAI9+uGdIuhjEV9bM8ImXKMMwRQAw3vYhcVM=
20260317120000_drop_refresh_requested_at.sql h1:R4jHtMkCpdY09orFA4RPvtLeUJ2Z7S4WPMmg/bDJuGg=
+20260318120000_migrate_excluded_users_to_bot_type.sql h1:QhLNJld3iqMSd/Zi8/jHadFB+0u4ocStzpprBqCaG+M=
+20260319120000_add_last_activity_at.sql h1:2evW+gCdRyH1OvTH2NDh5cV2xaxOHIldHYrkwS2ThE4=
diff --git a/db/tenant.sql b/db/tenant.sql
index 84404135..5671127f 100644
--- a/db/tenant.sql
+++ b/db/tenant.sql
@@ -4,7 +4,6 @@ CREATE TABLE `organization_settings` (
`release_detection_method` text NOT NULL DEFAULT 'branch',
`release_detection_key` text NOT NULL DEFAULT 'production',
`is_active` boolean NOT NULL DEFAULT true,
- `excluded_users` text NOT NULL DEFAULT '',
`timezone` text NOT NULL DEFAULT 'Asia/Tokyo',
`updated_at` datetime NOT NULL,
`created_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
@@ -161,6 +160,7 @@ CREATE TABLE `company_github_users` (
`display_name` text NOT NULL,
`type` text NULL,
`is_active` integer NOT NULL DEFAULT 0,
+ `last_activity_at` text NULL,
`updated_at` datetime NOT NULL,
`created_at` datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
PRIMARY KEY (`login`)