Skip to content

Commit 6040614

Browse files
maestro: show warning when refering scripts or dependencies that don't exist or will not be included in the zip
1 parent 7737f8e commit 6040614

File tree

2 files changed

+551
-0
lines changed

2 files changed

+551
-0
lines changed

src/providers/maestro.ts

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ export interface MaestroSocketMessage {
7979
payload: string;
8080
}
8181

82+
export interface MissingFileReference {
83+
flowFile: string;
84+
referencedFile: string;
85+
resolvedPath: string;
86+
}
87+
8288
export default class Maestro extends BaseProvider<MaestroOptions> {
8389
protected readonly URL = 'https://api.testingbot.com/v1/app-automate/maestro';
8490

@@ -405,6 +411,16 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
405411
this.logIncludedFiles(allFlowFiles, baseDir);
406412
}
407413

414+
// Check for missing file references and warn the user
415+
const missingReferences = await this.findMissingReferences(
416+
allFlowFiles,
417+
allFlowFiles,
418+
baseDir,
419+
);
420+
if (!this.options.quiet && missingReferences.length > 0) {
421+
this.logMissingReferences(missingReferences, baseDir);
422+
}
423+
408424
zipPath = await this.createFlowsZip(allFlowFiles, baseDir);
409425
shouldCleanup = true;
410426

@@ -773,6 +789,254 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
773789
return dependencies;
774790
}
775791

792+
/**
793+
* Find all file references in flow files that don't exist on disk.
794+
* This validates that all referenced files (runScript, runFlow, addMedia, etc.)
795+
* will be included in the zip.
796+
*/
797+
public async findMissingReferences(
798+
flowFiles: string[],
799+
allIncludedFiles: string[],
800+
baseDir?: string,
801+
): Promise<MissingFileReference[]> {
802+
const missingReferences: MissingFileReference[] = [];
803+
const includedFilesSet = new Set(allIncludedFiles.map((f) => path.resolve(f)));
804+
805+
for (const flowFile of flowFiles) {
806+
const ext = path.extname(flowFile).toLowerCase();
807+
if (ext !== '.yaml' && ext !== '.yml') {
808+
continue;
809+
}
810+
811+
try {
812+
const content = await fs.promises.readFile(flowFile, 'utf-8');
813+
const documents: unknown[] = [];
814+
yaml.loadAll(content, (doc) => documents.push(doc));
815+
816+
for (const flowData of documents) {
817+
if (flowData !== null && typeof flowData === 'object') {
818+
const missing = await this.findMissingInValue(
819+
flowData,
820+
flowFile,
821+
includedFilesSet,
822+
);
823+
missingReferences.push(...missing);
824+
}
825+
}
826+
} catch {
827+
// Ignore parsing errors
828+
}
829+
}
830+
831+
return missingReferences;
832+
}
833+
834+
/**
835+
* Recursively find missing file references in a YAML value
836+
*/
837+
private async findMissingInValue(
838+
value: unknown,
839+
flowFile: string,
840+
includedFiles: Set<string>,
841+
): Promise<MissingFileReference[]> {
842+
const missingReferences: MissingFileReference[] = [];
843+
844+
if (typeof value === 'string') {
845+
if (this.looksLikePath(value)) {
846+
const resolvedPath = path.resolve(path.dirname(flowFile), value);
847+
// Check if the file is in included files OR exists on disk
848+
if (!includedFiles.has(resolvedPath)) {
849+
try {
850+
await fs.promises.access(resolvedPath);
851+
// File exists on disk but won't be included - also warn
852+
} catch {
853+
// File doesn't exist
854+
missingReferences.push({
855+
flowFile,
856+
referencedFile: value,
857+
resolvedPath,
858+
});
859+
}
860+
}
861+
}
862+
} else if (Array.isArray(value)) {
863+
for (const item of value) {
864+
const missing = await this.findMissingInValue(
865+
item,
866+
flowFile,
867+
includedFiles,
868+
);
869+
missingReferences.push(...missing);
870+
}
871+
} else if (value !== null && typeof value === 'object') {
872+
const obj = value as Record<string, unknown>;
873+
const handledKeys = new Set<string>();
874+
875+
// Handle runScript - extract file reference but don't recurse
876+
// (runScript objects only contain file, env, when - no nested file refs)
877+
if ('runScript' in obj) {
878+
handledKeys.add('runScript');
879+
const runScript = obj.runScript;
880+
const scriptFile =
881+
typeof runScript === 'string'
882+
? runScript
883+
: (runScript as Record<string, unknown>)?.file;
884+
if (typeof scriptFile === 'string') {
885+
const resolved = path.resolve(path.dirname(flowFile), scriptFile);
886+
if (!includedFiles.has(resolved)) {
887+
try {
888+
await fs.promises.access(resolved);
889+
} catch {
890+
missingReferences.push({
891+
flowFile,
892+
referencedFile: scriptFile,
893+
resolvedPath: resolved,
894+
});
895+
}
896+
}
897+
}
898+
// Don't recurse into runScript - it only has file, env, when (no nested file refs)
899+
}
900+
901+
// Handle runFlow - extract file reference and recurse only into commands
902+
if ('runFlow' in obj) {
903+
handledKeys.add('runFlow');
904+
const runFlow = obj.runFlow;
905+
const flowRef =
906+
typeof runFlow === 'string'
907+
? runFlow
908+
: (runFlow as Record<string, unknown>)?.file;
909+
if (typeof flowRef === 'string') {
910+
const resolved = path.resolve(path.dirname(flowFile), flowRef);
911+
if (!includedFiles.has(resolved)) {
912+
try {
913+
await fs.promises.access(resolved);
914+
} catch {
915+
missingReferences.push({
916+
flowFile,
917+
referencedFile: flowRef,
918+
resolvedPath: resolved,
919+
});
920+
}
921+
}
922+
}
923+
// Only recurse into 'commands' if present (for inline commands)
924+
if (
925+
typeof runFlow === 'object' &&
926+
runFlow !== null &&
927+
'commands' in (runFlow as Record<string, unknown>)
928+
) {
929+
const commands = (runFlow as Record<string, unknown>).commands;
930+
if (Array.isArray(commands)) {
931+
const nestedMissing = await this.findMissingInValue(
932+
commands,
933+
flowFile,
934+
includedFiles,
935+
);
936+
missingReferences.push(...nestedMissing);
937+
}
938+
}
939+
}
940+
941+
// Handle addMedia
942+
if ('addMedia' in obj) {
943+
handledKeys.add('addMedia');
944+
const addMedia = obj.addMedia;
945+
const mediaFiles = Array.isArray(addMedia) ? addMedia : [addMedia];
946+
for (const mediaFile of mediaFiles) {
947+
if (typeof mediaFile === 'string') {
948+
const resolved = path.resolve(path.dirname(flowFile), mediaFile);
949+
if (!includedFiles.has(resolved)) {
950+
try {
951+
await fs.promises.access(resolved);
952+
} catch {
953+
missingReferences.push({
954+
flowFile,
955+
referencedFile: mediaFile,
956+
resolvedPath: resolved,
957+
});
958+
}
959+
}
960+
}
961+
}
962+
}
963+
964+
// Handle file property
965+
if ('file' in obj && typeof obj.file === 'string') {
966+
handledKeys.add('file');
967+
const resolved = path.resolve(path.dirname(flowFile), obj.file);
968+
if (!includedFiles.has(resolved)) {
969+
try {
970+
await fs.promises.access(resolved);
971+
} catch {
972+
missingReferences.push({
973+
flowFile,
974+
referencedFile: obj.file,
975+
resolvedPath: resolved,
976+
});
977+
}
978+
}
979+
}
980+
981+
// Handle onFlowStart, onFlowComplete, commands
982+
for (const key of ['onFlowStart', 'onFlowComplete', 'commands']) {
983+
if (key in obj) {
984+
handledKeys.add(key);
985+
const nested = obj[key];
986+
if (Array.isArray(nested)) {
987+
const nestedMissing = await this.findMissingInValue(
988+
nested,
989+
flowFile,
990+
includedFiles,
991+
);
992+
missingReferences.push(...nestedMissing);
993+
}
994+
}
995+
}
996+
997+
// Recursively check remaining properties
998+
for (const [key, propValue] of Object.entries(obj)) {
999+
if (!handledKeys.has(key)) {
1000+
const nestedMissing = await this.findMissingInValue(
1001+
propValue,
1002+
flowFile,
1003+
includedFiles,
1004+
);
1005+
missingReferences.push(...nestedMissing);
1006+
}
1007+
}
1008+
}
1009+
1010+
return missingReferences;
1011+
}
1012+
1013+
/**
1014+
* Log warnings for missing file references
1015+
*/
1016+
private logMissingReferences(
1017+
missingReferences: MissingFileReference[],
1018+
baseDir?: string,
1019+
): void {
1020+
if (missingReferences.length === 0) {
1021+
return;
1022+
}
1023+
1024+
logger.warn(
1025+
`Warning: ${missingReferences.length} referenced file(s) not found:`,
1026+
);
1027+
1028+
for (const ref of missingReferences) {
1029+
const flowRelative = baseDir
1030+
? path.relative(baseDir, ref.flowFile)
1031+
: path.basename(ref.flowFile);
1032+
logger.warn(` In ${flowRelative}: ${ref.referencedFile}`);
1033+
}
1034+
1035+
logger.warn(
1036+
'These files will not be included in the upload and may cause test failures.',
1037+
);
1038+
}
1039+
7761040
private logIncludedFiles(files: string[], baseDir?: string): void {
7771041
// Get relative paths for display
7781042
const relativePaths = files

0 commit comments

Comments
 (0)