1414)
1515from cumulusci .core .sfdx import sfdx
1616
17+ import tempfile
1718
1819class ScratchOrgConfig (SfdxOrgConfig ):
1920 """Salesforce DX Scratch org configuration"""
@@ -24,6 +25,7 @@ class ScratchOrgConfig(SfdxOrgConfig):
2425 password_failed : bool
2526 devhub : str
2627 release : str
28+ snapshot : str
2729
2830 createable : bool = True
2931
@@ -62,81 +64,119 @@ def days_alive(self) -> Optional[int]:
6264
6365 def create_org (self ) -> None :
6466 """Uses sf org create scratch to create the org"""
65- if not self .config_file :
66- raise ScratchOrgException (
67- f"Scratch org config { self .name } is missing a config_file"
67+ try :
68+ if not self .config_file :
69+ raise ScratchOrgException (
70+ f"Scratch org config { self .name } is missing a config_file"
71+ )
72+ if not self .scratch_org_type :
73+ self .config ["scratch_org_type" ] = "workspace"
74+
75+ args : List [str ] = self ._build_org_create_args ()
76+ extra_args = os .environ .get ("SFDX_ORG_CREATE_ARGS" , "" )
77+ p : sarge .Command = sfdx (
78+ f"org create scratch --json { extra_args } " ,
79+ args = args ,
80+ username = None ,
81+ log_note = "Creating scratch org" ,
6882 )
69- if not self .scratch_org_type :
70- self .config ["scratch_org_type" ] = "workspace"
83+ stdout = p .stdout_text .read ()
84+ stderr = p .stderr_text .read ()
85+
86+ def raise_error () -> NoReturn :
87+ message = f"{ FAILED_TO_CREATE_SCRATCH_ORG } : \n { stdout } \n { stderr } "
88+ try :
89+ output = json .loads (stdout )
90+ if (
91+ output .get ("message" ) == "The requested resource does not exist"
92+ and output .get ("name" ) == "NOT_FOUND"
93+ ):
94+ raise ScratchOrgException (
95+ "The Salesforce CLI was unable to create a scratch org. Ensure you are connected using a valid API version on an active Dev Hub."
96+ )
97+ except json .decoder .JSONDecodeError :
98+ raise ScratchOrgException (message )
7199
72- args : List [str ] = self ._build_org_create_args ()
73- extra_args = os .environ .get ("SFDX_ORG_CREATE_ARGS" , "" )
74- p : sarge .Command = sfdx (
75- f"org create scratch --json { extra_args } " ,
76- args = args ,
77- username = None ,
78- log_note = "Creating scratch org" ,
79- )
80- stdout = p .stdout_text .read ()
81- stderr = p .stderr_text .read ()
100+ raise ScratchOrgException (message )
82101
83- def raise_error () -> NoReturn :
84- message = f"{ FAILED_TO_CREATE_SCRATCH_ORG } : \n { stdout } \n { stderr } "
102+ result = {} # for type checker.
103+ if p .returncode :
104+ raise_error ()
85105 try :
86- output = json .loads (stdout )
87- if (
88- output .get ("message" ) == "The requested resource does not exist"
89- and output .get ("name" ) == "NOT_FOUND"
90- ):
91- raise ScratchOrgException (
92- "The Salesforce CLI was unable to create a scratch org. Ensure you are connected using a valid API version on an active Dev Hub."
93- )
106+ result = json .loads (stdout )
107+
94108 except json .decoder .JSONDecodeError :
95- raise ScratchOrgException ( message )
109+ raise_error ( )
96110
97- raise ScratchOrgException (message )
111+ if (
112+ not (res := result .get ("result" ))
113+ or ("username" not in res )
114+ or ("orgId" not in res )
115+ ):
116+ raise_error ()
98117
99- result = {} # for type checker.
100- if p .returncode :
101- raise_error ()
102- try :
103- result = json .loads (stdout )
104-
105- except json .decoder .JSONDecodeError :
106- raise_error ()
107-
108- if (
109- not (res := result .get ("result" ))
110- or ("username" not in res )
111- or ("orgId" not in res )
112- ):
113- raise_error ()
114-
115- if res ["username" ] is None :
116- raise ScratchOrgException (
117- "SFDX claimed to be successful but there was no username "
118- "in the output...maybe there was a gack?"
119- )
118+ if res ["username" ] is None :
119+ raise ScratchOrgException (
120+ "SFDX claimed to be successful but there was no username "
121+ "in the output...maybe there was a gack?"
122+ )
120123
121- self .config ["org_id" ] = res ["orgId" ]
122- self .config ["username" ] = res ["username" ]
124+ self .config ["org_id" ] = res ["orgId" ]
125+ self .config ["username" ] = res ["username" ]
123126
124- self .config ["date_created" ] = datetime .datetime .utcnow ()
127+ self .config ["date_created" ] = datetime .datetime .utcnow ()
125128
126- self .logger .error (stderr )
129+ self .logger .error (stderr )
127130
128- self .logger .info (
129- f"Created: OrgId: { self .config ['org_id' ]} , Username:{ self .config ['username' ]} "
130- )
131+ self .logger .info (
132+ f"Created: OrgId: { self .config ['org_id' ]} , Username:{ self .config ['username' ]} "
133+ )
131134
132- if self .config .get ("set_password" ):
133- self .generate_password ()
135+ if self .config .get ("set_password" ):
136+ self .generate_password ()
134137
135- # Flag that this org has been created
136- self .config ["created" ] = True
138+ # Flag that this org has been created
139+ self .config ["created" ] = True
140+ finally :
141+ # Clean up temporary config file if it exists
142+ if hasattr (self , '_tmp_config' ) and self ._tmp_config and os .path .exists (self ._tmp_config ):
143+ try :
144+ os .unlink (self ._tmp_config )
145+ except Exception as e :
146+ self .logger .warning (f"Failed to clean up temporary config file: { e } " )
137147
138148 def _build_org_create_args (self ) -> List [str ]:
139- args = ["-f" , self .config_file , "-w" , "120" ]
149+ config_file = self .config_file
150+ self ._tmp_config = None
151+ if self .snapshot and self .config_file :
152+ # When using snapshot, remove features, edition and snapshot from config
153+ with open (self .config_file , "r" ) as f :
154+ org_config = json .load (f )
155+ org_config .pop ("features" , None )
156+ org_config .pop ("edition" , None )
157+ org_config .pop ("snapshot" , None )
158+
159+ # Create temporary config file
160+ tmp = tempfile .NamedTemporaryFile (mode = 'w' , suffix = '.json' , delete = False )
161+ self ._tmp_config = tmp .name
162+
163+ # Try catch error here to avoid leaving temp file around
164+ try :
165+ json .dump (org_config , tmp , indent = 4 )
166+ tmp .close ()
167+ config_file = tmp .name
168+ self ._tmp_config = config_file
169+ except Exception :
170+ tmp_name = tmp .name
171+ try :
172+ tmp .close ()
173+ except Exception :
174+ pass
175+ if os .path .exists (tmp_name ):
176+ os .remove (tmp_name )
177+ raise
178+
179+ args = ["-f" , config_file , "-w" , "120" ]
140180 devhub_username : Optional [str ] = self ._choose_devhub_username ()
141181 if devhub_username :
142182 args += ["--target-dev-hub" , devhub_username ]
@@ -157,6 +197,8 @@ def _build_org_create_args(self) -> List[str]:
157197 args += [f"--admin-email={ self .email_address } " ]
158198 if self .default :
159199 args += ["--set-default" ]
200+ if self .snapshot :
201+ args += [f"--snapshot={ self .snapshot } " ]
160202
161203 return args
162204
0 commit comments