@@ -3,6 +3,32 @@ import { promptMultipleSelect } from "@std/cli/unstable-prompt-multiple-select";
33import { gray , green , yellow } from "@std/fmt/colors" ;
44import { createTrpcClient } from "../auth.ts" ;
55import type { GlobalContext } from "../main.ts" ;
6+ import { error , ExitCode , isNonInteractive } from "../util.ts" ;
7+
8+ export interface SetupAwsOptions {
9+ /** AWS IAM policy ARNs to attach. When set, the interactive multi-select is skipped. */
10+ policies ?: string [ ] ;
11+ /** Use this IAM role name instead of generating a random-suffixed one. Enables idempotent re-runs. */
12+ roleName ?: string ;
13+ }
14+
15+ export interface SetupGcpOptions {
16+ /** GCP IAM role names to grant. When set, the interactive multi-select is skipped. */
17+ roles ?: string [ ] ;
18+ /** Use this service-account name instead of generating a random-suffixed one. Enables idempotent re-runs. */
19+ serviceAccountName ?: string ;
20+ /** Auto-accept the API-enable prompt for any missing required APIs. */
21+ enableApis ?: boolean ;
22+ }
23+
24+ /**
25+ * Apply-confirmation helper. In non-interactive mode (`--yes`/`--non-interactive`
26+ * or no TTY) we proceed automatically; otherwise we still prompt the human.
27+ */
28+ function confirmApply ( context : GlobalContext , message : string ) : boolean {
29+ if ( isNonInteractive ( context ) ) return true ;
30+ return confirm ( message ) ;
31+ }
632
733const AWS_OIDC_AUDIENCE = "sts.amazonaws.com" ;
834
@@ -110,6 +136,7 @@ export async function setupAws(
110136 org : string ,
111137 app : string ,
112138 contexts : string [ ] ,
139+ opts : SetupAwsOptions = { } ,
113140) {
114141 // Print out "AWS Setup Wizard for Deno Deploy" in an orange box
115142 console . log (
@@ -173,51 +200,66 @@ export async function setupAws(
173200 ) ,
174201 ) ;
175202
176- log ( gray ( " Loading IAM policies..." ) ) ;
177- const allPolicies = await runAwsCommand < {
178- Policies : Array < { PolicyName : string ; Arn : string } > ;
179- } > ( [ "iam" , "list-policies" ] ) ;
180- log ( "\r" ) ;
181-
182- const choices = allPolicies . Policies . map ( ( policy ) => ( {
183- label : policy . PolicyName ,
184- value : policy . Arn ,
185- } ) ) ;
186-
187- let policies ;
188- while ( true ) {
189- const result = promptMultipleSelect (
190- "Select permission policies you want to attach to the new role" ,
191- choices ,
203+ let policies : Array < { label : string ; value : string } > ;
204+ if ( opts . policies !== undefined ) {
205+ // Flag path: trust the caller, skip the listing and the prompt entirely.
206+ policies = opts . policies . map ( ( arn ) => ( { label : arn , value : arn } ) ) ;
207+ } else if ( isNonInteractive ( context ) ) {
208+ error (
209+ context ,
210+ "Selecting AWS policies requires interactive input.\nUse --policies <arn> (repeatable) to pre-supply policies." ,
192211 {
193- clear : true ,
194- fitToRemainingHeight : true ,
212+ code : ExitCode . USAGE ,
213+ errorCode : "MISSING_FLAG" ,
214+ hint : "Pass --policies <arn> for each policy you want attached." ,
195215 } ,
196216 ) ;
217+ } else {
218+ log ( gray ( " Loading IAM policies..." ) ) ;
219+ const allPolicies = await runAwsCommand < {
220+ Policies : Array < { PolicyName : string ; Arn : string } > ;
221+ } > ( [ "iam" , "list-policies" ] ) ;
222+ log ( "\r" ) ;
223+
224+ const choices = allPolicies . Policies . map ( ( policy ) => ( {
225+ label : policy . PolicyName ,
226+ value : policy . Arn ,
227+ } ) ) ;
228+
229+ while ( true ) {
230+ const result = promptMultipleSelect (
231+ "Select permission policies you want to attach to the new role" ,
232+ choices ,
233+ {
234+ clear : true ,
235+ fitToRemainingHeight : true ,
236+ } ,
237+ ) ;
197238
198- if ( result === null ) {
199- console . log ( "%c Exiting setup." , "color: yellow;" ) ;
200- Deno . exit ( 1 ) ;
201- }
239+ if ( result === null ) {
240+ console . log ( "%c Exiting setup." , "color: yellow;" ) ;
241+ Deno . exit ( 1 ) ;
242+ }
202243
203- if ( result . length === 0 ) {
204- const confirmNoPolicies = confirm (
205- "Are you sure you don't want to associate any policies? Remember to use Space to select a policy, and Enter to confirm your selections." ,
206- ) ;
207- if ( ! confirmNoPolicies ) {
208- continue ;
244+ if ( result . length === 0 ) {
245+ const confirmNoPolicies = confirm (
246+ "Are you sure you don't want to associate any policies? Remember to use Space to select a policy, and Enter to confirm your selections." ,
247+ ) ;
248+ if ( ! confirmNoPolicies ) {
249+ continue ;
250+ }
251+ console . log (
252+ "%c No policies selected. You can attach policies later through the AWS Console." ,
253+ "color: yellow;" ,
254+ ) ;
209255 }
210- console . log (
211- "%c No policies selected. You can attach policies later through the AWS Console." ,
212- "color: yellow;" ,
213- ) ;
214- }
215256
216- policies = result ;
217- break ;
257+ policies = result ;
258+ break ;
259+ }
218260 }
219261
220- const roleName = `DenoDeploy-${ org } -${ app } -${
262+ const roleName = opts . roleName ?? `DenoDeploy-${ org } -${ app } -${
221263 Math . random ( )
222264 . toString ( 36 )
223265 . substring ( 2 , 8 )
@@ -285,7 +327,7 @@ export async function setupAws(
285327
286328 console . log ( "" ) ;
287329
288- if ( ! confirm ( "Do you want to apply these changes?" ) ) {
330+ if ( ! confirmApply ( context , "Do you want to apply these changes?" ) ) {
289331 console . log ( "%c Exiting setup." , "color: yellow;" ) ;
290332 Deno . exit ( 1 ) ;
291333 }
@@ -405,6 +447,7 @@ export async function setupGcp(
405447 org : string ,
406448 app : string ,
407449 contexts : string [ ] ,
450+ opts : SetupGcpOptions = { } ,
408451) {
409452 // Print out "GCP Setup Wizard for Deno Deploy" in a blue box
410453 console . log (
@@ -499,7 +542,9 @@ export async function setupGcp(
499542 }
500543 console . log ( "" ) ;
501544
502- const enableApis = confirm ( "Do you want to enable these APIs now?" ) ;
545+ const enableApis = opts . enableApis ||
546+ isNonInteractive ( context ) ||
547+ confirm ( "Do you want to enable these APIs now?" ) ;
503548
504549 if ( ! enableApis ) {
505550 console . log (
@@ -568,58 +613,78 @@ export async function setupGcp(
568613 ) ,
569614 ) ;
570615
571- // List available IAM roles for selection
572- log ( gray ( " Loading IAM roles..." ) ) ;
573- const roles = await runGcloudCommand < Array < { name : string ; title : string } > > (
574- [ "iam" , "roles" , "list" , "--filter=stage:GA" ] ,
575- ) ;
576- log ( "\r" ) ;
577-
578- const roleChoices = roles . map ( ( role ) => ( {
579- label : `${ role . title } (${ role . name . split ( "/" ) . pop ( ) } )` ,
580- value : role . name ,
581- } ) ) ;
582-
583- let selectedRoles ;
584- while ( true ) {
585- const result = promptMultipleSelect (
586- "Select IAM roles you want to grant to the service account" ,
587- roleChoices ,
616+ let selectedRoles : Array < { label : string ; value : string } > ;
617+ if ( opts . roles !== undefined ) {
618+ selectedRoles = opts . roles . map ( ( role ) => ( { label : role , value : role } ) ) ;
619+ } else if ( isNonInteractive ( context ) ) {
620+ error (
621+ context ,
622+ "Selecting GCP roles requires interactive input.\nUse --roles <role> (repeatable) to pre-supply roles." ,
588623 {
589- clear : true ,
590- fitToRemainingHeight : true ,
624+ code : ExitCode . USAGE ,
625+ errorCode : "MISSING_FLAG" ,
626+ hint : "Pass --roles <role> for each role you want granted." ,
591627 } ,
592628 ) ;
629+ } else {
630+ log ( gray ( " Loading IAM roles..." ) ) ;
631+ const roles = await runGcloudCommand <
632+ Array < { name : string ; title : string } >
633+ > ( [ "iam" , "roles" , "list" , "--filter=stage:GA" ] ) ;
634+ log ( "\r" ) ;
635+
636+ const roleChoices = roles . map ( ( role ) => ( {
637+ label : `${ role . title } (${ role . name . split ( "/" ) . pop ( ) } )` ,
638+ value : role . name ,
639+ } ) ) ;
640+
641+ while ( true ) {
642+ const result = promptMultipleSelect (
643+ "Select IAM roles you want to grant to the service account" ,
644+ roleChoices ,
645+ {
646+ clear : true ,
647+ fitToRemainingHeight : true ,
648+ } ,
649+ ) ;
593650
594- if ( result === null ) {
595- console . log ( "%c Exiting setup." , "color: yellow;" ) ;
596- Deno . exit ( 1 ) ;
597- }
651+ if ( result === null ) {
652+ console . log ( "%c Exiting setup." , "color: yellow;" ) ;
653+ Deno . exit ( 1 ) ;
654+ }
598655
599- if ( result . length === 0 ) {
600- const confirmNoRoles = confirm (
601- "Are you sure you don't want to associate any roles? Remember to use Space to select a role, and Enter to confirm your selections." ,
602- ) ;
603- if ( ! confirmNoRoles ) {
604- continue ;
656+ if ( result . length === 0 ) {
657+ const confirmNoRoles = confirm (
658+ "Are you sure you don't want to associate any roles? Remember to use Space to select a role, and Enter to confirm your selections." ,
659+ ) ;
660+ if ( ! confirmNoRoles ) {
661+ continue ;
662+ }
663+ console . log (
664+ "%c No roles selected. You can grant roles later through the GCP Console." ,
665+ "color: yellow;" ,
666+ ) ;
605667 }
606- console . log (
607- "%c No roles selected. You can grant roles later through the GCP Console." ,
608- "color: yellow;" ,
609- ) ;
610- }
611668
612- selectedRoles = result ;
613- break ;
669+ selectedRoles = result ;
670+ break ;
671+ }
614672 }
615673
616- // service account name must be between 6 and 30 characters, lowercase, and can contain letters, numbers, and dashes
617- let serviceAccountName = "deno-" ;
618- const orgPart = org . slice ( 0 , 8 ) . replaceAll ( / - + $ / g, "" ) ;
619- const appPart = app . slice ( 0 , 17 - orgPart . length ) . replaceAll ( / - + $ / g, "" ) ;
620- serviceAccountName += `${ orgPart } -${ appPart } -${
621- Math . random ( ) . toString ( 36 ) . substring ( 2 , 8 )
622- } `;
674+ // Service-account name must be 6-30 chars, lowercase, [a-z0-9-]. With
675+ // --service-account-name we trust the caller; otherwise we derive a
676+ // random-suffixed default to avoid colliding with existing user resources.
677+ let serviceAccountName : string ;
678+ if ( opts . serviceAccountName !== undefined ) {
679+ serviceAccountName = opts . serviceAccountName ;
680+ } else {
681+ serviceAccountName = "deno-" ;
682+ const orgPart = org . slice ( 0 , 8 ) . replaceAll ( / - + $ / g, "" ) ;
683+ const appPart = app . slice ( 0 , 17 - orgPart . length ) . replaceAll ( / - + $ / g, "" ) ;
684+ serviceAccountName += `${ orgPart } -${ appPart } -${
685+ Math . random ( ) . toString ( 36 ) . substring ( 2 , 8 )
686+ } `;
687+ }
623688
624689 const serviceAccountEmail =
625690 `${ serviceAccountName } @${ projectId } .iam.gserviceaccount.com` ;
@@ -696,7 +761,7 @@ export async function setupGcp(
696761
697762 console . log ( "" ) ;
698763
699- if ( ! confirm ( "Do you want to apply these changes?" ) ) {
764+ if ( ! confirmApply ( context , "Do you want to apply these changes?" ) ) {
700765 console . log ( "%c Exiting setup." , "color: yellow;" ) ;
701766 Deno . exit ( 1 ) ;
702767 }
0 commit comments