diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1e459a5..faacc11 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -97,7 +97,7 @@ GithubWorkflowTool is a C++/Qt6 desktop application that simulates GitHub Action #### JobExecutor - **Purpose**: Orchestrate workflow execution - **Key Features**: - - Topological sort for job dependencies + - Gated dependency tree that skips blocked branches while continuing independent jobs - Backend selection (Container/QEMU) - Real-time progress reporting - Error handling and recovery @@ -201,9 +201,9 @@ GithubWorkflowTool is a C++/Qt6 desktop application that simulates GitHub Action ↓ 4. MatrixStrategy expands matrix jobs ↓ -5. JobExecutor resolves job dependencies +5. JobExecutor resolves job dependencies as a gated tree ↓ -6. For each job in order: +6. For each ready job (while skipping branches with failed needs): a. Select backend (Container/QEMU) b. Backend prepares environment c. For each step: diff --git a/include/core/JobExecutor.h b/include/core/JobExecutor.h index a8a7cc3..663bec1 100644 --- a/include/core/JobExecutor.h +++ b/include/core/JobExecutor.h @@ -60,11 +60,6 @@ private: * @brief Execute a single job */ bool executeJob(const WorkflowJob& job); - - /** - * @brief Resolve job dependencies (needs) - */ - QStringList resolveJobOrder(const Workflow& workflow) const; }; } // namespace core diff --git a/src/core/JobExecutor.cpp b/src/core/JobExecutor.cpp index 255ce45..8b3327a 100644 --- a/src/core/JobExecutor.cpp +++ b/src/core/JobExecutor.cpp @@ -2,6 +2,9 @@ #include "backends/ContainerBackend.h" #include "backends/QemuBackend.h" #include +#include +#include +#include namespace gwt { namespace core { @@ -37,24 +40,96 @@ bool JobExecutor::executeWorkflow(const Workflow& workflow, emit stepOutput("", "", text); }); - // Resolve job execution order - QStringList jobOrder = resolveJobOrder(workflow); - - bool success = true; - for (const QString& jobId : jobOrder) { - const WorkflowJob& job = workflow.jobs[jobId]; - - emit jobStarted(jobId); - - if (!executeJob(job)) { - success = false; - emit jobFinished(jobId, false); - break; + // Build dependency graph for gated execution + QMap dependencies; + QMap dependents; + QStringList readyQueue; + QSet queued; + + for (auto it = workflow.jobs.begin(); it != workflow.jobs.end(); ++it) { + const QString& jobId = it.key(); + const QStringList& needs = it.value().needs; + dependencies[jobId] = needs; + + if (needs.isEmpty()) { + readyQueue << jobId; + queued.insert(jobId); + } + + for (const QString& dep : needs) { + dependents[dep] << jobId; } - - emit jobFinished(jobId, true); } - + + if (readyQueue.isEmpty()) { + emit error("No runnable jobs found. Check for circular or missing dependencies."); + m_running = false; + return false; + } + + QSet processed; + QSet failed; + bool success = true; + + while (!readyQueue.isEmpty()) { + QString jobId = readyQueue.takeFirst(); + queued.remove(jobId); + + const WorkflowJob& job = workflow.jobs[jobId]; + + emit jobStarted(jobId); + + bool jobSuccess = executeJob(job); + emit jobFinished(jobId, jobSuccess); + + processed.insert(jobId); + if (!jobSuccess) { + failed.insert(jobId); + success = false; + } + + // Evaluate dependents and gate execution based on dependency outcomes + for (const QString& dependentId : dependents[jobId]) { + if (processed.contains(dependentId) || queued.contains(dependentId)) { + continue; + } + + const QStringList& needs = dependencies[dependentId]; + + bool depsProcessed = true; + bool depsFailed = false; + for (const QString& dep : needs) { + if (!processed.contains(dep)) { + depsProcessed = false; + break; + } + if (failed.contains(dep)) { + depsFailed = true; + } + } + + if (!depsProcessed) { + continue; + } + + if (depsFailed) { + emit error(QStringLiteral("Skipping %1 because a dependency failed").arg(dependentId)); + emit jobFinished(dependentId, false); + processed.insert(dependentId); + failed.insert(dependentId); + success = false; + } else { + readyQueue << dependentId; + queued.insert(dependentId); + } + } + } + + if (processed.size() != workflow.jobs.size()) { + emit error("Workflow contains unresolved dependencies or cycles"); + success = false; + } + m_running = false; emit executionFinished(success); @@ -78,66 +153,24 @@ bool JobExecutor::executeJob(const WorkflowJob& job) { emit error("Failed to prepare environment for: " + job.runsOn); return false; } - + // Execute steps for (const WorkflowStep& step : job.steps) { emit stepStarted(job.id, step.name); - + QVariantMap context; context["env"] = job.env; context["workingDirectory"] = step.workingDirectory; - + if (!m_backend->executeStep(step, context)) { emit stepFinished(job.id, step.name, false); return false; } - + emit stepFinished(job.id, step.name, true); } - - return true; -} -QStringList JobExecutor::resolveJobOrder(const Workflow& workflow) const { - QStringList order; - QSet processed; - QMap dependencies; - - // Build dependency map - for (auto it = workflow.jobs.begin(); it != workflow.jobs.end(); ++it) { - dependencies[it.key()] = it.value().needs; - } - - // Simple topological sort - bool changed = true; - while (changed && processed.size() < workflow.jobs.size()) { - changed = false; - - for (auto it = workflow.jobs.begin(); it != workflow.jobs.end(); ++it) { - QString jobId = it.key(); - - if (processed.contains(jobId)) { - continue; - } - - // Check if all dependencies are satisfied - bool canRun = true; - for (const QString& dep : dependencies[jobId]) { - if (!processed.contains(dep)) { - canRun = false; - break; - } - } - - if (canRun) { - order << jobId; - processed.insert(jobId); - changed = true; - } - } - } - - return order; + return true; } } // namespace core