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