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}
-
- - -

- Copilot is excluded by default. Add additional usernames to exclude - from cycle time calculations. -

-
{fields.excludedUsers.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`)