33import { env } from "@/env.mjs" ;
44import { ErrorCode } from "@/lib/errorCodes" ;
55import { notAuthenticated , notFound , orgNotFound , secretAlreadyExists , ServiceError , ServiceErrorException , unexpectedError } from "@/lib/serviceError" ;
6- import { CodeHostType , isServiceError } from "@/lib/utils" ;
6+ import { CodeHostType , isHttpError , isServiceError } from "@/lib/utils" ;
77import { prisma } from "@/prisma" ;
88import { render } from "@react-email/components" ;
99import * as Sentry from '@sentry/nextjs' ;
@@ -22,6 +22,7 @@ import { StatusCodes } from "http-status-codes";
2222import { cookies , headers } from "next/headers" ;
2323import { createTransport } from "nodemailer" ;
2424import { auth } from "./auth" ;
25+ import { Octokit } from "octokit" ;
2526import { getConnection } from "./data/connection" ;
2627import { IS_BILLING_ENABLED } from "./ee/features/billing/stripe" ;
2728import InviteUserEmail from "./emails/inviteUserEmail" ;
@@ -790,6 +791,144 @@ export const createConnection = async (name: string, type: CodeHostType, connect
790791 } , OrgRole . OWNER )
791792 ) ) ;
792793
794+ export const experimental_addGithubRepositoryByUrl = async ( repositoryUrl : string , domain : string ) : Promise < { connectionId : number } | ServiceError > => sew ( ( ) =>
795+ withAuth ( ( userId ) =>
796+ withOrgMembership ( userId , domain , async ( { org } ) => {
797+ if ( env . EXPERIMENT_SELF_SERVE_REPO_INDEXING_ENABLED !== 'true' ) {
798+ return {
799+ statusCode : StatusCodes . BAD_REQUEST ,
800+ errorCode : ErrorCode . INVALID_REQUEST_BODY ,
801+ message : "This feature is not enabled." ,
802+ } satisfies ServiceError ;
803+ }
804+
805+ // Parse repository URL to extract owner/repo
806+ const repoInfo = ( ( ) => {
807+ const url = repositoryUrl . trim ( ) ;
808+
809+ // Handle various GitHub URL formats
810+ const patterns = [
811+ // https://github.com/owner/repo or https://github.com/owner/repo.git
812+ / ^ h t t p s ? : \/ \/ g i t h u b \. c o m \/ ( [ a - z A - Z 0 - 9 _ . - ] + ) \/ ( [ a - z A - Z 0 - 9 _ . - ] + ?) (?: \. g i t ) ? \/ ? $ / ,
813+ // github.com/owner/repo
814+ / ^ g i t h u b \. c o m \/ ( [ a - z A - Z 0 - 9 _ . - ] + ) \/ ( [ a - z A - Z 0 - 9 _ . - ] + ?) (?: \. g i t ) ? \/ ? $ / ,
815+ // owner/repo
816+ / ^ ( [ a - z A - Z 0 - 9 _ . - ] + ) \/ ( [ a - z A - Z 0 - 9 _ . - ] + ) $ /
817+ ] ;
818+
819+ for ( const pattern of patterns ) {
820+ const match = url . match ( pattern ) ;
821+ if ( match ) {
822+ return {
823+ owner : match [ 1 ] ,
824+ repo : match [ 2 ]
825+ } ;
826+ }
827+ }
828+
829+ return null ;
830+ } ) ( ) ;
831+
832+ if ( ! repoInfo ) {
833+ return {
834+ statusCode : StatusCodes . BAD_REQUEST ,
835+ errorCode : ErrorCode . INVALID_REQUEST_BODY ,
836+ message : "Invalid repository URL format. Please use 'owner/repo' or 'https://github.com/owner/repo' format." ,
837+ } satisfies ServiceError ;
838+ }
839+
840+ const { owner, repo } = repoInfo ;
841+
842+ // Use GitHub API to fetch repository information and get the external_id
843+ const octokit = new Octokit ( {
844+ auth : env . EXPERIMENT_SELF_SERVE_REPO_INDEXING_GITHUB_TOKEN
845+ } ) ;
846+
847+ let githubRepo ;
848+ try {
849+ const response = await octokit . rest . repos . get ( {
850+ owner,
851+ repo,
852+ } ) ;
853+ githubRepo = response . data ;
854+ } catch ( error ) {
855+ if ( isHttpError ( error , 404 ) ) {
856+ return {
857+ statusCode : StatusCodes . NOT_FOUND ,
858+ errorCode : ErrorCode . INVALID_REQUEST_BODY ,
859+ message : `Repository '${ owner } /${ repo } ' not found or is private. Only public repositories can be added.` ,
860+ } satisfies ServiceError ;
861+ }
862+
863+ if ( isHttpError ( error , 403 ) ) {
864+ return {
865+ statusCode : StatusCodes . FORBIDDEN ,
866+ errorCode : ErrorCode . INVALID_REQUEST_BODY ,
867+ message : `Access to repository '${ owner } /${ repo } ' is forbidden. Only public repositories can be added.` ,
868+ } satisfies ServiceError ;
869+ }
870+
871+ return {
872+ statusCode : StatusCodes . INTERNAL_SERVER_ERROR ,
873+ errorCode : ErrorCode . INVALID_REQUEST_BODY ,
874+ message : `Failed to fetch repository information: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
875+ } satisfies ServiceError ;
876+ }
877+
878+ if ( githubRepo . private ) {
879+ return {
880+ statusCode : StatusCodes . BAD_REQUEST ,
881+ errorCode : ErrorCode . INVALID_REQUEST_BODY ,
882+ message : "Only public repositories can be added." ,
883+ } satisfies ServiceError ;
884+ }
885+
886+ // Check if this repository is already connected using the external_id
887+ const existingRepo = await prisma . repo . findFirst ( {
888+ where : {
889+ orgId : org . id ,
890+ external_id : githubRepo . id . toString ( ) ,
891+ external_codeHostType : 'github' ,
892+ external_codeHostUrl : 'https://github.com' ,
893+ }
894+ } ) ;
895+
896+ if ( existingRepo ) {
897+ return {
898+ statusCode : StatusCodes . BAD_REQUEST ,
899+ errorCode : ErrorCode . CONNECTION_ALREADY_EXISTS ,
900+ message : "This repository already exists." ,
901+ } satisfies ServiceError ;
902+ }
903+
904+ const connectionName = `${ owner } -${ repo } -${ Date . now ( ) } ` ;
905+
906+ // Create GitHub connection config
907+ const connectionConfig : GithubConnectionConfig = {
908+ type : "github" as const ,
909+ repos : [ `${ owner } /${ repo } ` ] ,
910+ ...( env . EXPERIMENT_SELF_SERVE_REPO_INDEXING_GITHUB_TOKEN ? {
911+ token : {
912+ env : 'EXPERIMENT_SELF_SERVE_REPO_INDEXING_GITHUB_TOKEN'
913+ }
914+ } : { } )
915+ } ;
916+
917+ const connection = await prisma . connection . create ( {
918+ data : {
919+ orgId : org . id ,
920+ name : connectionName ,
921+ config : connectionConfig as unknown as Prisma . InputJsonValue ,
922+ connectionType : 'github' ,
923+ }
924+ } ) ;
925+
926+ return {
927+ connectionId : connection . id ,
928+ }
929+ } , OrgRole . GUEST ) , /* allowAnonymousAccess = */ true
930+ ) ) ;
931+
793932export const updateConnectionDisplayName = async ( connectionId : number , name : string , domain : string ) : Promise < { success : boolean } | ServiceError > => sew ( ( ) =>
794933 withAuth ( ( userId ) =>
795934 withOrgMembership ( userId , domain , async ( { org } ) => {
0 commit comments