@@ -243,6 +243,7 @@ describe("deploy", () => {
243243 domainId ?: string ;
244244 productionConfig ?: Record < string , unknown > ;
245245 developmentConfig ?: Record < string , unknown > ;
246+ cnameTargets ?: readonly { host : string ; value : string ; required : boolean } [ ] ;
246247 } = { } ,
247248 ) {
248249 const instanceId = options . instanceId ?? "ins_prod_mock" ;
@@ -254,6 +255,9 @@ describe("deploy", () => {
254255 const productionConfig = options . productionConfig ?? {
255256 connection_oauth_google : { enabled : false , client_id : "" , client_secret : "" } ,
256257 } ;
258+ const cnameTargets = options . cnameTargets ?? [
259+ { host : `clerk.${ domain } ` , value : "frontend-api.clerk.services" , required : true } ,
260+ ] ;
257261
258262 mockFetchApplication . mockResolvedValue ( {
259263 application_id : "app_xyz789" ,
@@ -282,9 +286,7 @@ describe("deploy", () => {
282286 frontend_api_url : `https://clerk.${ domain } ` ,
283287 accounts_portal_url : `https://accounts.${ domain } ` ,
284288 development_origin : "" ,
285- cname_targets : [
286- { host : `clerk.${ domain } ` , value : "frontend-api.clerk.services" , required : true } ,
287- ] ,
289+ cname_targets : cnameTargets ,
288290 created_at : "2026-05-06T00:00:00Z" ,
289291 updated_at : "2026-05-06T00:00:00Z" ,
290292 } ,
@@ -647,7 +649,10 @@ describe("deploy", () => {
647649 test ( "DNS setup prints dashboard handoff and asks about verifying DNS" , async ( ) => {
648650 await linkedProject ( ) ;
649651 mockIsAgent . mockReturnValue ( false ) ;
650- mockConfirm . mockResolvedValueOnce ( true ) . mockResolvedValueOnce ( true ) ;
652+ mockConfirm
653+ . mockResolvedValueOnce ( true ) // Proceed?
654+ . mockResolvedValueOnce ( true ) // Create production instance?
655+ . mockResolvedValueOnce ( false ) ; // Export DNS records as a BIND zone file?
651656 mockSelect . mockResolvedValueOnce ( "skip" ) . mockResolvedValueOnce ( "skip" ) ;
652657 mockInput . mockResolvedValueOnce ( "example.com" ) ;
653658
@@ -663,11 +668,15 @@ describe("deploy", () => {
663668 expect ( err ) . toContain ( "propagation and SSL issuance" ) ;
664669 expect ( err ) . toContain ( "DNS propagation can take time" ) ;
665670 expect ( err ) . toContain ( "Skipping DNS verification for now." ) ;
666- expect ( mockConfirm ) . toHaveBeenCalledTimes ( 2 ) ;
671+ expect ( mockConfirm ) . toHaveBeenCalledTimes ( 3 ) ;
667672 expect ( mockConfirm ) . toHaveBeenCalledWith ( {
668673 message : "Create production instance?" ,
669674 default : true ,
670675 } ) ;
676+ expect ( mockConfirm ) . toHaveBeenCalledWith ( {
677+ message : "Export DNS records as a BIND zone file?" ,
678+ default : false ,
679+ } ) ;
671680 expect ( mockConfirm ) . not . toHaveBeenCalledWith ( {
672681 message : "Continue to OAuth setup?" ,
673682 default : true ,
@@ -937,6 +946,119 @@ describe("deploy", () => {
937946 expect ( recordsIdx ) . toBeLessThan ( promptIdx ) ;
938947 } ) ;
939948
949+ test ( "BIND export prompt writes the zone file when the user accepts" , async ( ) => {
950+ await linkedProject ( {
951+ instances : { development : "ins_dev_123" , production : "ins_prod_123" } ,
952+ } ) ;
953+ mockIsAgent . mockReturnValue ( false ) ;
954+ mockLiveProduction ( {
955+ instanceId : "ins_prod_123" ,
956+ productionConfig : { } ,
957+ } ) ;
958+ mockGetDeployStatus . mockResolvedValue ( {
959+ status : "incomplete" ,
960+ dns_ok : false ,
961+ ssl_ok : false ,
962+ mail_ok : false ,
963+ } ) ;
964+ mockConfirm . mockResolvedValueOnce ( true ) ; // BIND export prompt: yes
965+ mockSelect . mockResolvedValueOnce ( "skip" ) . mockResolvedValueOnce ( "have-credentials" ) ;
966+ mockInput . mockResolvedValueOnce ( "google-client-id.apps.googleusercontent.com" ) ;
967+ mockPassword . mockResolvedValueOnce ( "google-secret" ) ;
968+ mockPatchInstanceConfig . mockResolvedValueOnce ( { } ) ;
969+
970+ const writeSpy = spyOn ( Bun , "write" ) . mockResolvedValue ( 0 ) ;
971+ try {
972+ await runDeploy ( { } ) ;
973+ const err = stripAnsi ( captured . err ) ;
974+
975+ const zoneCall = writeSpy . mock . calls . find ( ( call ) => String ( call [ 0 ] ) . endsWith ( ".zone" ) ) ;
976+ expect ( zoneCall ) . toBeDefined ( ) ;
977+ const pathArg = zoneCall ! [ 0 ] ;
978+ const contentArg = zoneCall ! [ 1 ] ;
979+ expect ( String ( pathArg ) ) . toMatch ( / c l e r k - e x a m p l e \. c o m \. z o n e $ / ) ;
980+ expect ( String ( contentArg ) ) . toContain ( "$ORIGIN example.com." ) ;
981+ expect ( String ( contentArg ) ) . toContain ( "$TTL 300" ) ;
982+ expect ( String ( contentArg ) ) . toContain ( "IN\tCNAME" ) ;
983+ expect ( err ) . toContain ( "Wrote " ) ;
984+ expect ( err ) . toContain ( "clerk-example.com.zone" ) ;
985+ } finally {
986+ writeSpy . mockRestore ( ) ;
987+ }
988+ } ) ;
989+
990+ test ( "BIND export prompt writes no file when the user declines" , async ( ) => {
991+ await linkedProject ( {
992+ instances : { development : "ins_dev_123" , production : "ins_prod_123" } ,
993+ } ) ;
994+ mockIsAgent . mockReturnValue ( false ) ;
995+ mockLiveProduction ( {
996+ instanceId : "ins_prod_123" ,
997+ productionConfig : { } ,
998+ } ) ;
999+ mockGetDeployStatus . mockResolvedValue ( {
1000+ status : "incomplete" ,
1001+ dns_ok : false ,
1002+ ssl_ok : false ,
1003+ mail_ok : false ,
1004+ } ) ;
1005+ mockConfirm . mockResolvedValueOnce ( false ) ; // BIND export prompt: no
1006+ mockSelect . mockResolvedValueOnce ( "skip" ) . mockResolvedValueOnce ( "have-credentials" ) ;
1007+ mockInput . mockResolvedValueOnce ( "google-client-id.apps.googleusercontent.com" ) ;
1008+ mockPassword . mockResolvedValueOnce ( "google-secret" ) ;
1009+ mockPatchInstanceConfig . mockResolvedValueOnce ( { } ) ;
1010+
1011+ const writeSpy = spyOn ( Bun , "write" ) . mockResolvedValue ( 0 ) ;
1012+ try {
1013+ await runDeploy ( { } ) ;
1014+ const err = stripAnsi ( captured . err ) ;
1015+
1016+ const zoneCall = writeSpy . mock . calls . find ( ( call ) => String ( call [ 0 ] ) . endsWith ( ".zone" ) ) ;
1017+ expect ( zoneCall ) . toBeUndefined ( ) ;
1018+ expect ( err ) . not . toContain ( "Wrote " ) ;
1019+ } finally {
1020+ writeSpy . mockRestore ( ) ;
1021+ }
1022+ } ) ;
1023+
1024+ test ( "BIND export prompt is skipped when cnameTargets is empty" , async ( ) => {
1025+ await linkedProject ( {
1026+ instances : { development : "ins_dev_123" , production : "ins_prod_123" } ,
1027+ } ) ;
1028+ mockIsAgent . mockReturnValue ( false ) ;
1029+ mockLiveProduction ( {
1030+ instanceId : "ins_prod_123" ,
1031+ productionConfig : { } ,
1032+ cnameTargets : [ ] , // override: domain has no CNAME targets
1033+ } ) ;
1034+ mockGetDeployStatus . mockResolvedValue ( {
1035+ status : "incomplete" ,
1036+ dns_ok : false ,
1037+ ssl_ok : false ,
1038+ mail_ok : false ,
1039+ } ) ;
1040+ mockSelect . mockResolvedValueOnce ( "skip" ) . mockResolvedValueOnce ( "have-credentials" ) ;
1041+ mockInput . mockResolvedValueOnce ( "google-client-id.apps.googleusercontent.com" ) ;
1042+ mockPassword . mockResolvedValueOnce ( "google-secret" ) ;
1043+ mockPatchInstanceConfig . mockResolvedValueOnce ( { } ) ;
1044+
1045+ const writeSpy = spyOn ( Bun , "write" ) . mockResolvedValue ( 0 ) ;
1046+ try {
1047+ await runDeploy ( { } ) ;
1048+
1049+ // confirm() was never called for the BIND prompt in this run.
1050+ const bindPromptCalls = mockConfirm . mock . calls . filter ( ( call ) => {
1051+ const arg = call [ 0 ] as { message ?: string } | undefined ;
1052+ return typeof arg ?. message === "string" && arg . message . includes ( "BIND zone file" ) ;
1053+ } ) ;
1054+ expect ( bindPromptCalls . length ) . toBe ( 0 ) ;
1055+ const zoneCall = writeSpy . mock . calls . find ( ( call ) => String ( call [ 0 ] ) . endsWith ( ".zone" ) ) ;
1056+ expect ( zoneCall ) . toBeUndefined ( ) ;
1057+ } finally {
1058+ writeSpy . mockRestore ( ) ;
1059+ }
1060+ } ) ;
1061+
9401062 test ( "DNS verification timeout names the specific pending components from deploy_status" , async ( ) => {
9411063 await linkedProject ( {
9421064 instances : { development : "ins_dev_123" , production : "ins_prod_123" } ,
0 commit comments