@@ -194,6 +194,9 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
194194 const maestroOptions = this . options . getMaestroOptions ( ) ;
195195 const metadata = this . options . metadata ;
196196
197+ // Process flows to show actual zip structure
198+ const flowResult = await this . collectFlows ( ) ;
199+
197200 this . printDryRunSummary ( {
198201 provider : 'Maestro' ,
199202 apiUrl : this . URL ,
@@ -219,6 +222,21 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
219222 } ,
220223 } ) ;
221224
225+ // Show zip structure details
226+ if ( flowResult ) {
227+ const { allFlowFiles, baseDir } = flowResult ;
228+ const effectiveBase =
229+ baseDir || this . computeCommonDirectory ( allFlowFiles ) ;
230+ logger . info ( 'Zip structure (files as they will appear in the archive):' ) ;
231+ for ( const file of allFlowFiles ) {
232+ const archiveName = path . relative ( effectiveBase , path . resolve ( file ) ) ;
233+ logger . info ( ` ${ archiveName } ` ) ;
234+ }
235+ logger . info ( ` Base directory: ${ path . resolve ( effectiveBase ) } ` ) ;
236+ } else {
237+ logger . info ( 'Flows: single .zip file (uploaded as-is)' ) ;
238+ }
239+
222240 return { success : true , runs : [ ] } ;
223241 }
224242
@@ -413,26 +431,28 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
413431 }
414432 }
415433
416- private async uploadFlows ( ) {
434+ /**
435+ * Collect and resolve all flow files, their dependencies, and determine the
436+ * base directory for the zip structure. This is shared by both uploadFlows
437+ * and the dry-run path.
438+ *
439+ * Returns null if the input is a single .zip file (direct upload, no processing).
440+ */
441+ async collectFlows ( ) : Promise < {
442+ allFlowFiles : string [ ] ;
443+ baseDir : string | undefined ;
444+ } | null > {
417445 const flowsPaths = this . options . flows ;
418446
419- let zipPath : string ;
420-
421- // Special case: single zip file - upload directly
447+ // Special case: single zip file - no processing needed
422448 if ( flowsPaths . length === 1 ) {
423449 const singlePath = flowsPaths [ 0 ] ;
424450 const stat = await fs . promises . stat ( singlePath ) . catch ( ( ) => null ) ;
425- if ( stat ?. isFile ( ) && path . extname ( singlePath ) . toLowerCase ( ) === '.zip' ) {
426- zipPath = singlePath ;
427- await this . upload . upload ( {
428- filePath : zipPath ,
429- url : `${ this . URL } /${ this . appId } /tests` ,
430- credentials : this . credentials ,
431- contentType : 'application/zip' ,
432- showProgress : ! this . options . quiet ,
433- validateZipFormat : true ,
434- } ) ;
435- return true ;
451+ if (
452+ stat ?. isFile ( ) &&
453+ path . extname ( singlePath ) . toLowerCase ( ) === '.zip'
454+ ) {
455+ return null ;
436456 }
437457 }
438458
@@ -489,8 +509,25 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
489509 }
490510
491511 // Determine base directory for zip structure
492- // If we have a single directory, use it as base; otherwise use common ancestor or flatten
493- const baseDir = baseDirs . length === 1 ? baseDirs [ 0 ] : undefined ;
512+ // If we have a single directory, use it as base; otherwise try to find the Maestro project root
513+ let baseDir = baseDirs . length === 1 ? baseDirs [ 0 ] : undefined ;
514+
515+ // When individual files are passed (not a directory), search ancestor directories
516+ // for config.yaml to find the Maestro project root. This ensures the zip preserves
517+ // the full directory structure needed for relative runFlow paths (e.g., ../../screens/).
518+ if ( ! baseDir && allFlowFiles . length > 0 ) {
519+ const projectRoot = await this . findMaestroProjectRoot ( allFlowFiles ) ;
520+ if ( projectRoot ) {
521+ baseDir = projectRoot . dir ;
522+ // Include config.yaml and discover its dependencies
523+ const configResolved = path . resolve ( projectRoot . configPath ) ;
524+ if (
525+ ! allFlowFiles . some ( ( f ) => path . resolve ( f ) === configResolved )
526+ ) {
527+ allFlowFiles . push ( projectRoot . configPath ) ;
528+ }
529+ }
530+ }
494531
495532 // Discover dependencies (addMedia, runScript, runFlow, etc.) for all flow files
496533 // This ensures referenced files are included even when individual YAML files are passed
@@ -536,7 +573,28 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
536573 this . logMissingReferences ( missingReferences , baseDir ) ;
537574 }
538575
539- zipPath = await this . createFlowsZip ( allFlowFiles , baseDir ) ;
576+ return { allFlowFiles, baseDir } ;
577+ }
578+
579+ private async uploadFlows ( ) {
580+ const result = await this . collectFlows ( ) ;
581+
582+ if ( result === null ) {
583+ // Single zip file - upload directly
584+ const zipPath = this . options . flows [ 0 ] ;
585+ await this . upload . upload ( {
586+ filePath : zipPath ,
587+ url : `${ this . URL } /${ this . appId } /tests` ,
588+ credentials : this . credentials ,
589+ contentType : 'application/zip' ,
590+ showProgress : ! this . options . quiet ,
591+ validateZipFormat : true ,
592+ } ) ;
593+ return true ;
594+ }
595+
596+ const { allFlowFiles, baseDir } = result ;
597+ const zipPath = await this . createFlowsZip ( allFlowFiles , baseDir ) ;
540598
541599 try {
542600 await this . upload . upload ( {
@@ -553,6 +611,35 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
553611 return true ;
554612 }
555613
614+ /**
615+ * Search ancestor directories of the given flow files for a Maestro config file
616+ * (config.yaml or config.yml). This identifies the project root so the zip
617+ * preserves the directory structure needed for relative paths like ../../screens/.
618+ */
619+ private async findMaestroProjectRoot (
620+ flowFiles : string [ ] ,
621+ ) : Promise < { dir : string ; configPath : string } | null > {
622+ // Start from the first flow file and walk up
623+ const startDir = path . dirname ( path . resolve ( flowFiles [ 0 ] ) ) ;
624+ const rootDir = path . parse ( startDir ) . root ;
625+
626+ let currentDir = startDir ;
627+ while ( currentDir !== rootDir ) {
628+ for ( const configName of [ 'config.yaml' , 'config.yml' ] ) {
629+ const candidatePath = path . join ( currentDir , configName ) ;
630+ try {
631+ await fs . promises . access ( candidatePath ) ;
632+ return { dir : currentDir , configPath : candidatePath } ;
633+ } catch {
634+ // Config not found here, keep searching
635+ }
636+ }
637+ currentDir = path . dirname ( currentDir ) ;
638+ }
639+
640+ return null ;
641+ }
642+
556643 private async discoverFlows ( directory : string ) : Promise < string [ ] > {
557644 const entries = await fs . promises . readdir ( directory , {
558645 withFileTypes : true ,
@@ -901,8 +988,19 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
901988 }
902989
903990 // Recursively check remaining object properties for nested structures
991+ // Skip config-only keys that contain path-like strings but aren't runtime
992+ // file dependencies (e.g., executionOrder.flowsOrder, flows glob patterns)
993+ const configOnlyKeys = new Set ( [
994+ 'executionOrder' ,
995+ 'flows' ,
996+ 'tags' ,
997+ 'includeTags' ,
998+ 'excludeTags' ,
999+ 'env' ,
1000+ ] ) ;
1001+
9041002 for ( const [ key , propValue ] of Object . entries ( obj ) ) {
905- if ( ! handledKeys . has ( key ) ) {
1003+ if ( ! handledKeys . has ( key ) && ! configOnlyKeys . has ( key ) ) {
9061004 const deps = await this . extractPathsFromValue (
9071005 propValue ,
9081006 flowFile ,
@@ -1101,8 +1199,19 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
11011199 }
11021200
11031201 // Recursively check remaining properties
1202+ // Skip config-only keys that contain path-like strings but aren't runtime
1203+ // file dependencies (e.g., executionOrder.flowsOrder, flows glob patterns)
1204+ const configOnlyKeys = new Set ( [
1205+ 'executionOrder' ,
1206+ 'flows' ,
1207+ 'tags' ,
1208+ 'includeTags' ,
1209+ 'excludeTags' ,
1210+ 'env' ,
1211+ ] ) ;
1212+
11041213 for ( const [ key , propValue ] of Object . entries ( obj ) ) {
1105- if ( ! handledKeys . has ( key ) ) {
1214+ if ( ! handledKeys . has ( key ) && ! configOnlyKeys . has ( key ) ) {
11061215 const nestedMissing = await this . findMissingInValue (
11071216 propValue ,
11081217 flowFile ,
0 commit comments