3
3
import { env } from "@/env.mjs" ;
4
4
import { ErrorCode } from "@/lib/errorCodes" ;
5
5
import { 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" ;
7
7
import { prisma } from "@/prisma" ;
8
8
import { render } from "@react-email/components" ;
9
9
import * as Sentry from '@sentry/nextjs' ;
@@ -22,6 +22,7 @@ import { StatusCodes } from "http-status-codes";
22
22
import { cookies , headers } from "next/headers" ;
23
23
import { createTransport } from "nodemailer" ;
24
24
import { auth } from "./auth" ;
25
+ import { Octokit } from "octokit" ;
25
26
import { getConnection } from "./data/connection" ;
26
27
import { IS_BILLING_ENABLED } from "./ee/features/billing/stripe" ;
27
28
import InviteUserEmail from "./emails/inviteUserEmail" ;
@@ -790,6 +791,144 @@ export const createConnection = async (name: string, type: CodeHostType, connect
790
791
} , OrgRole . OWNER )
791
792
) ) ;
792
793
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
+
793
932
export const updateConnectionDisplayName = async ( connectionId : number , name : string , domain : string ) : Promise < { success : boolean } | ServiceError > => sew ( ( ) =>
794
933
withAuth ( ( userId ) =>
795
934
withOrgMembership ( userId , domain , async ( { org } ) => {
0 commit comments