From 938f00e2c4090564deaaca63b427c923c550e99e Mon Sep 17 00:00:00 2001 From: coji Date: Tue, 7 Apr 2026 12:58:12 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E3=83=AA=E3=83=9D=E3=82=B8=E3=83=88?= =?UTF-8?q?=E3=83=AA=E8=BF=BD=E5=8A=A0=E6=99=82=E3=81=AB=E5=88=9D=E5=9B=9E?= =?UTF-8?q?=20crawl=20=E3=82=92=E8=87=AA=E5=8B=95=20trigger=20(#274)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit リポ追加直後にデータが表示されるよう、該当 repo 限定の crawl を fire-and-forget で trigger する。concurrencyKey は per-repo (`crawl:${orgId}:${repoId}`) + coalesce: 'skip' で、連続追加時に 異なる repo の crawl が drop されないようにする。 併せて crawl.server.ts の repositoryId 不一致時を throw → warn + 早期 return に緩和。追加直後に削除された場合の race でジョブが失敗しない。 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../+functions/mutations.server.ts | 3 +- .../settings/repositories.add/index.tsx | 41 ++++++++++++++++--- app/services/jobs/concurrency-keys.server.ts | 2 + app/services/jobs/crawl.server.ts | 7 +++- 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/app/routes/$orgSlug/settings/repositories.add/+functions/mutations.server.ts b/app/routes/$orgSlug/settings/repositories.add/+functions/mutations.server.ts index b5862375..1eef51a1 100644 --- a/app/routes/$orgSlug/settings/repositories.add/+functions/mutations.server.ts +++ b/app/routes/$orgSlug/settings/repositories.add/+functions/mutations.server.ts @@ -40,5 +40,6 @@ export const addRepository = async ( updatedAt: eb.ref('excluded.updatedAt'), })), ) - .executeTakeFirst() + .returning('id') + .executeTakeFirstOrThrow() } diff --git a/app/routes/$orgSlug/settings/repositories.add/index.tsx b/app/routes/$orgSlug/settings/repositories.add/index.tsx index ee0cd65c..44f01c09 100644 --- a/app/routes/$orgSlug/settings/repositories.add/index.tsx +++ b/app/routes/$orgSlug/settings/repositories.add/index.tsx @@ -26,10 +26,13 @@ import { Stack, } from '~/app/components/ui' import { requireOrgOwner } from '~/app/libs/auth.server' +import { captureExceptionToSentry } from '~/app/libs/sentry-node.server' import { orgContext } from '~/app/middleware/context' import { clearOrgCache, getOrgCachedData } from '~/app/services/cache.server' +import { durably } from '~/app/services/durably.server' import { getGithubAppLink } from '~/app/services/github-integration-queries.server' import { resolveOctokitFromOrg } from '~/app/services/github-octokit.server' +import { crawlRepoConcurrencyKey } from '~/app/services/jobs/concurrency-keys.server' import type { OrganizationId } from '~/app/types/organization' import ContentSection from '../+components/content-section' import { RepositoryItem, RepositoryList } from './+components' @@ -211,19 +214,45 @@ export const action = async ({ request, context }: Route.ActionArgs) => { return dataWithError({}, { message: 'Invalid form submission' }) } - try { - await addRepository(organization.id, { - owner: submission.value.owner, - repo: submission.value.name, - }) - } catch (e) { + const inserted = await addRepository(organization.id, { + owner: submission.value.owner, + repo: submission.value.name, + }).catch((e) => { console.error('Failed to add repository:', e) + return null + }) + if (!inserted) { return dataWithError( {}, { message: 'Failed to add repository. Please try again.' }, ) } + // Fire-and-forget: kick off an initial crawl so existing PRs appear without + // waiting for the hourly scheduled job. Failures must not fail the add. + durably.jobs.crawl + .trigger( + { + organizationId: organization.id, + refresh: false, + repositoryId: inserted.id, + }, + { + concurrencyKey: crawlRepoConcurrencyKey(organization.id, inserted.id), + labels: { organizationId: organization.id }, + coalesce: 'skip', + }, + ) + .catch((e) => { + captureExceptionToSentry(e, { + tags: { component: 'repositories.add', operation: 'crawl.trigger' }, + extra: { + organizationId: organization.id, + repositoryId: inserted.id, + }, + }) + }) + return dataWithSuccess( {}, { diff --git a/app/services/jobs/concurrency-keys.server.ts b/app/services/jobs/concurrency-keys.server.ts index aab30870..d62efdee 100644 --- a/app/services/jobs/concurrency-keys.server.ts +++ b/app/services/jobs/concurrency-keys.server.ts @@ -1,3 +1,5 @@ export const crawlConcurrencyKey = (orgId: string) => `crawl:${orgId}` as const +export const crawlRepoConcurrencyKey = (orgId: string, repoId: string) => + `crawl:${orgId}:${repoId}` as const export const processConcurrencyKey = (orgId: string) => `process:${orgId}` as const diff --git a/app/services/jobs/crawl.server.ts b/app/services/jobs/crawl.server.ts index f186cf43..e3975076 100644 --- a/app/services/jobs/crawl.server.ts +++ b/app/services/jobs/crawl.server.ts @@ -71,7 +71,12 @@ export const crawlJob = defineJob({ ? organization.repositories.filter((r) => r.id === input.repositoryId) : organization.repositories if (input.repositoryId && targetRepos.length === 0) { - throw new Error('repositoryId does not match any organization repository') + // Repository may have been deleted between trigger and run. Skip instead + // of throwing so transient races (e.g. add-then-remove) don't fail the job. + step.log.warn( + `repositoryId ${input.repositoryId} does not match any organization repository; skipping crawl`, + ) + return { fetchedRepos: 0, pullCount: 0, failedRepos: [] } } const repoCount = targetRepos.length