@@ -162,7 +162,7 @@ func RenderComponents(env *azldev.Env, options *RenderOptions) ([]*RenderResult,
162162 results := make ([]* RenderResult , len (componentList ))
163163
164164 // ── Phase 1: Parallel source preparation ──
165- prepared := parallelPrepare (env , componentList , stagingDir , results )
165+ prepared := parallelPrepare (env , componentList , stagingDir , options . OutputDir , options . Force , results )
166166
167167 // ── Phase 2: Batch mock processing ──
168168 mockResultMap := batchMockProcess (env , mockProcessor , stagingDir , prepared )
@@ -255,6 +255,8 @@ func parallelPrepare(
255255 env * azldev.Env ,
256256 comps []components.Component ,
257257 stagingDir string ,
258+ outputDir string ,
259+ allowOverwrite bool ,
258260 results []* RenderResult ,
259261) []* preparedComponent {
260262 progressEvent := env .StartEvent ("Preparing component sources" , "count" , len (comps ))
@@ -274,7 +276,7 @@ func parallelPrepare(
274276 go func (idx int , comp components.Component ) {
275277 defer waitGroup .Done ()
276278
277- resultsChan <- prepWithSemaphore (workerEnv , semaphore , idx , comp , stagingDir )
279+ resultsChan <- prepWithSemaphore (workerEnv , semaphore , idx , comp , stagingDir , outputDir , allowOverwrite )
278280 }(compIdx , comp )
279281 }
280282
@@ -308,25 +310,20 @@ func prepWithSemaphore(
308310 index int ,
309311 comp components.Component ,
310312 stagingDir string ,
313+ outputDir string ,
314+ allowOverwrite bool ,
311315) prepResult {
312- // Validate component name before any filesystem work.
313- if err := validateComponentName (comp .GetName ()); err != nil {
314- return prepResult {index : index , result : & RenderResult {
315- Component : comp .GetName (),
316- OutputDir : "(invalid)" ,
317- Status : renderStatusError ,
318- Error : err .Error (),
319- }}
320- }
316+ componentName := comp .GetName ()
317+ compOutputDir := filepath .Join (outputDir , componentName )
321318
322319 // Context-aware semaphore acquisition.
323320 select {
324321 case semaphore <- struct {}{}:
325322 defer func () { <- semaphore }()
326323 case <- env .Done ():
327324 return prepResult {index : index , result : & RenderResult {
328- Component : comp . GetName () ,
329- OutputDir : comp . GetName () ,
325+ Component : componentName ,
326+ OutputDir : compOutputDir ,
330327 Status : renderStatusCancelled ,
331328 Error : "context cancelled" ,
332329 }}
@@ -335,11 +332,21 @@ func prepWithSemaphore(
335332 prep , err := prepareComponentSources (env , comp , stagingDir )
336333 if err != nil {
337334 slog .Error ("Failed to prepare component sources" ,
338- "component" , comp .GetName (), "error" , err )
335+ "component" , componentName , "error" , err )
336+
337+ // Write error marker so the failure is visible in git diff.
338+ if allowOverwrite {
339+ if removeErr := env .FS ().RemoveAll (compOutputDir ); removeErr != nil {
340+ slog .Debug ("Failed to clean output before writing error marker" ,
341+ "path" , compOutputDir , "error" , removeErr )
342+ }
343+ }
344+
345+ writeRenderErrorMarker (env .FS (), compOutputDir )
339346
340347 return prepResult {index : index , result : & RenderResult {
341- Component : comp . GetName () ,
342- OutputDir : comp . GetName () ,
348+ Component : componentName ,
349+ OutputDir : compOutputDir ,
343350 Status : renderStatusError ,
344351 Error : err .Error (),
345352 }}
@@ -388,7 +395,7 @@ func prepareComponentSources(
388395 // WithSkipLookaside avoids expensive tarball downloads — only spec +
389396 // sidecar files are needed for rendering.
390397 preparerOpts := []sources.PreparerOption {
391- sources .WithGitRepo (),
398+ sources .WithGitRepo (env . Config (). Project . DefaultAuthorEmail ),
392399 sources .WithSkipLookaside (),
393400 }
394401
@@ -839,19 +846,6 @@ func validateOutputDir(outputDir string) error {
839846 return nil
840847}
841848
842- // validateComponentName rejects component names that could cause path traversal
843- // when used as directory names in filepath.Join.
844- func validateComponentName (name string ) error {
845- if name == "" || name == "." ||
846- strings .Contains (name , "/" ) || strings .Contains (name , "\\ " ) ||
847- strings .Contains (name , ".." ) || strings .ContainsRune (name , 0 ) {
848- return fmt .Errorf (
849- "component name %#q is invalid or contains path separators/traversal sequences" , name )
850- }
851-
852- return nil
853- }
854-
855849// createMockProcessor creates a [sources.MockProcessor] using the project's
856850// mock config. Returns nil if the mock config is not available (e.g., no project
857851// config loaded, or no mock config path configured).
0 commit comments