@factorialco/shadowdog
Version:
<img src="https://raw.githubusercontent.com/factorialco/shadowdog/refs/heads/main/logo.png" alt="drawing" width="100"/>
124 lines (123 loc) • 5.42 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generate = void 0;
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const plugins_1 = require("./plugins");
const task_runner_1 = require("./task-runner");
const tasks_1 = require("./tasks");
const utils_1 = require("./utils");
// Wait for artifact files to be written and readable
const waitForArtifacts = async (artifacts) => {
// Make max retries configurable via environment variable for faster CI tests
const maxRetries = process.env.SHADOWDOG_ARTIFACT_WAIT_MAX_RETRIES
? parseInt(process.env.SHADOWDOG_ARTIFACT_WAIT_MAX_RETRIES, 10)
: 50; // Default: 5 seconds max wait time
const retryDelay = 100; // 100ms between retries
for (const artifact of artifacts) {
const artifactPath = path_1.default.join(process.cwd(), artifact.output);
let retries = 0;
while (retries < maxRetries) {
try {
// Check if file exists and is readable
await fs_1.default.promises.access(artifactPath, fs_1.default.constants.F_OK | fs_1.default.constants.R_OK);
// For files, also verify they have content (not empty)
const stats = await fs_1.default.promises.stat(artifactPath);
if (stats.isFile() && stats.size === 0) {
throw new Error('File is empty');
}
// File exists and is readable with content, move to next artifact
break;
}
catch {
// File doesn't exist or isn't readable yet, wait and retry
await new Promise((resolve) => setTimeout(resolve, retryDelay));
retries++;
}
}
if (retries >= maxRetries) {
// Fail the build if artifact is not available after max retries
// This ensures we catch cases where commands don't produce expected outputs
throw new Error(`Artifact '${artifact.output}' was not created or is not readable after task completion. ` +
`Waited ${(maxRetries * retryDelay) / 1000} seconds.`);
}
}
};
const processTask = async (task, pluginsConfig, eventEmitter, options) => {
switch (task.type) {
case 'parallel': {
return Promise.all(task.tasks.map((subTask) => processTask(subTask, pluginsConfig, eventEmitter, options)));
}
case 'serial': {
for (const subTask of task.tasks) {
await processTask(subTask, pluginsConfig, eventEmitter, options);
}
return;
}
case 'command': {
eventEmitter.emit('begin', {
artifacts: task.config.artifacts,
});
const taskRunner = new task_runner_1.TaskRunner({
files: task.files,
environment: task.environment,
config: task.config,
eventEmitter,
});
(0, plugins_1.filterMiddlewarePlugins)(pluginsConfig).forEach(({ fn, options: pluginOptions }) => {
taskRunner.use(fn.middleware, pluginOptions);
});
taskRunner.use(() => {
return (0, tasks_1.runTask)({
command: task.config.command,
workingDirectory: path_1.default.join(process.cwd(), task.config.workingDirectory),
onSpawn: () => { },
onExit: () => { },
});
});
try {
await taskRunner.execute();
// Wait for all artifacts to be written and readable before proceeding
// This ensures dependent tasks can read the updated artifact files
await waitForArtifacts(task.config.artifacts);
eventEmitter.emit('end', {
artifacts: task.config.artifacts,
});
}
catch (error) {
eventEmitter.emit('error', {
artifacts: task.config.artifacts,
errorMessage: error.message,
});
if (!options.continueOnError) {
throw error;
}
}
break;
}
case 'empty': {
// noop
}
}
};
const generate = async (config, eventEmitter, options) => {
const plugins = (0, plugins_1.filterCommandPlugins)(config.plugins);
const task = {
type: 'parallel',
tasks: config.watchers.flatMap((watcherConfig) => {
const files = (0, utils_1.processFiles)(watcherConfig.files, [...watcherConfig.ignored, ...config.defaultIgnoredFiles], true); // Enable preserveNonExistent for dependency tracking
return watcherConfig.commands.map((commandConfig) => ({
type: 'command',
config: commandConfig,
files,
environment: watcherConfig.environment,
}));
}),
};
const finalTask = plugins.reduce((subTask, { fn }) => fn.command(subTask), task);
return processTask(finalTask, config.plugins, eventEmitter, options);
};
exports.generate = generate;