Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export const OrganizationSettings = ({
releaseDetectionMethod: string
releaseDetectionKey: string
isActive: number
excludedUsers: string
timezone: string
language: string
}
Expand All @@ -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,
},
Expand Down Expand Up @@ -173,21 +171,6 @@ export const OrganizationSettings = ({
<div className="text-destructive">{fields.isActive.errors}</div>
</fieldset>

<fieldset className="space-y-1">
<Label htmlFor={fields.excludedUsers.id}>
Excluded Users (comma separated)
</Label>
<Input
{...getInputProps(fields.excludedUsers, { type: 'text' })}
placeholder="e.g. my-org-bot, some-other-bot"
/>
<p className="text-muted-foreground text-sm">
Copilot is excluded by default. Add additional usernames to exclude
from cycle time calculations.
</p>
<div className="text-destructive">{fields.excludedUsers.errors}</div>
</fieldset>

{form.errors && (
<Alert variant="destructive">
<AlertTitle>System Error</AlertTitle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export const updateOrganizationSetting = async (
| 'releaseDetectionMethod'
| 'releaseDetectionKey'
| 'isActive'
| 'excludedUsers'
| 'timezone'
| 'language'
>,
Expand Down
1 change: 0 additions & 1 deletion app/routes/$orgSlug/settings/_index/+schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}),
Expand Down
2 changes: 0 additions & 2 deletions app/routes/$orgSlug/settings/_index/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export const action = async ({ request, context }: Route.ActionArgs) => {
releaseDetectionMethod,
releaseDetectionKey,
isActive,
excludedUsers,
timezone,
language,
} = submission.value
Expand All @@ -55,7 +54,6 @@ export const action = async ({ request, context }: Route.ActionArgs) => {
releaseDetectionMethod,
releaseDetectionKey,
isActive,
excludedUsers,
timezone,
language,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,18 +149,18 @@ export function createColumns(timezone: string): ColumnDef<GithubUserRow>[] {
),
},
{
accessorKey: 'createdAt',
accessorKey: 'lastActivityAt',
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Created" />
),
cell: ({ row }) => (
<div className="text-muted-foreground text-nowrap">
{dayjs
.utc(row.getValue('createdAt'))
.tz(timezone)
.format('YYYY-MM-DD')}
</div>
<DataTableColumnHeader column={column} title="Last Activity" />
),
cell: ({ row }) => {
const value = row.getValue<string | null>('lastActivityAt')
return (
<div className="text-muted-foreground text-nowrap">
{value ? dayjs.utc(value).tz(timezone).fromNow() : '-'}
</div>
)
},
},
{
id: 'actions',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const SortSchema = z.object({
z
.union([z.literal('asc'), z.literal('desc')])
.optional()
.default('asc'),
.default('desc'),
),
})

Expand Down
15 changes: 13 additions & 2 deletions app/routes/$orgSlug/settings/github-users._index/queries.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const listFilteredGithubUsers = async ({
'companyGithubUsers.displayName',
'companyGithubUsers.type',
'companyGithubUsers.isActive',
'companyGithubUsers.lastActivityAt',
'companyGithubUsers.createdAt',
])

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 ?? '',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down
9 changes: 6 additions & 3 deletions app/services/jobs/analyze-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface WorkerInput {
repositoryId: string
releaseDetectionMethod: string
releaseDetectionKey: string
excludedUsers: string
botLogins: string[]
filterPrNumbers?: number[]
}

Expand Down Expand Up @@ -47,11 +47,14 @@ const result: Awaited<ReturnType<typeof buildPullRequests>> =
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],
})
1 change: 1 addition & 0 deletions app/services/jobs/crawl.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const crawlJob = defineJob({
}
return {
organizationSetting: org.organizationSetting,
botLogins: org.botLogins,
repositories: org.repositories,
exportSetting: org.exportSetting,
}
Expand Down
1 change: 1 addition & 0 deletions app/services/jobs/recalculate.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const recalculateJob = defineJob({
}
return {
organizationSetting: org.organizationSetting,
botLogins: org.botLogins,
repositories: org.repositories,
exportSetting: org.exportSetting,
}
Expand Down
2 changes: 1 addition & 1 deletion app/services/jobs/run-in-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface AnalyzeWorkerInput {
repositoryId: string
releaseDetectionMethod: string
releaseDetectionKey: string
excludedUsers: string
botLogins: string[]
filterPrNumbers?: number[]
}

Expand Down
9 changes: 7 additions & 2 deletions app/services/jobs/shared-steps.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ interface AnalyzeResult {
reviews: AnalyzedReview[]
reviewers: AnalyzedReviewer[]
reviewResponses: AnalyzedReviewResponse[]
botUsers: string[]
}

interface OrganizationData {
organizationSetting: Pick<
Selectable<TenantDB.OrganizationSettings>,
'releaseDetectionMethod' | 'releaseDetectionKey' | 'excludedUsers'
'releaseDetectionMethod' | 'releaseDetectionKey'
>
botLogins: string[]
repositories: Selectable<TenantDB.Repositories>[]
exportSetting?: Selectable<TenantDB.ExportSettings> | null
}
Expand Down Expand Up @@ -107,6 +109,7 @@ export async function analyzeAndFinalizeSteps(
const allReviews: AnalyzedReview[] = []
const allReviewers: AnalyzedReviewer[] = []
const allReviewResponses: AnalyzedReviewResponse[] = []
const allBotUsers = new Set<string>()
const sqliteBusyEvents: SqliteBusyEvent[] = []

for (let i = 0; i < organization.repositories.length; i++) {
Expand All @@ -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,
},
{
Expand All @@ -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
Expand All @@ -151,6 +155,7 @@ export async function analyzeAndFinalizeSteps(
pulls: allPulls,
reviews: allReviews,
reviewers: allReviewers,
botUsers: allBotUsers,
})
})
})
Expand Down
2 changes: 1 addition & 1 deletion app/services/tenant-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface CompanyGithubUsers {
createdAt: Generated<string>;
displayName: string;
isActive: 0 | 1;
lastActivityAt: string | null;
login: string;
type: string | null;
updatedAt: string;
Expand Down Expand Up @@ -70,7 +71,6 @@ export interface Integrations {

export interface OrganizationSettings {
createdAt: Generated<string>;
excludedUsers: Generated<string>;
id: string;
isActive: Generated<0 | 1>;
language: Generated<"en" | "ja">;
Expand Down
53 changes: 52 additions & 1 deletion batch/db/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export async function batchReplacePullRequestReviewers(
export async function upsertCompanyGithubUsers(
organizationId: OrganizationId,
logins: string[],
botUsers?: Set<string>,
) {
if (logins.length === 0) return

Expand All @@ -240,6 +241,7 @@ export async function upsertCompanyGithubUsers(
uniqueLogins.map((login) => ({
login,
displayName: login,
type: botUsers?.has(login) ? 'Bot' : null,
isActive: 0,
updatedAt: now,
})),
Expand All @@ -252,6 +254,39 @@ export async function upsertCompanyGithubUsers(
)
}

function trackLatest(map: Map<string, string>, login: string, ts: string) {
const key = login.toLowerCase()
if (!map.has(key) || ts > (map.get(key) ?? '')) {
map.set(key, ts)
}
}

/**
* ユーザーごとの最終活動日時を更新する。
* 既存値より新しい場合のみ上書き。
*/
async function updateLastActivityAt(
organizationId: OrganizationId,
lastActivity: Map<string, string>,
) {
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 処理。
Expand All @@ -262,6 +297,7 @@ export async function upsertAnalyzedData(
pulls: Selectable<TenantDB.PullRequests>[]
reviews: AnalyzedReview[]
reviewers: AnalyzedReviewer[]
botUsers?: Set<string>
},
) {
// Auto-register discovered GitHub users
Expand All @@ -277,7 +313,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<string, string>()
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)
Expand Down
Loading