mirror of
https://github.com/johndoe6345789/GithubWorkflowTool.git
synced 2026-04-26 22:55:05 +00:00
Initial implementation of GithubWorkflowTool
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
133
src/backends/ContainerBackend.cpp
Normal file
133
src/backends/ContainerBackend.cpp
Normal file
@@ -0,0 +1,133 @@
|
||||
#include "backends/ContainerBackend.h"
|
||||
#include <QProcess>
|
||||
#include <QDebug>
|
||||
|
||||
namespace gwt {
|
||||
namespace backends {
|
||||
|
||||
ContainerBackend::ContainerBackend(QObject* parent)
|
||||
: ExecutionBackend(parent)
|
||||
{
|
||||
detectRuntime();
|
||||
}
|
||||
|
||||
ContainerBackend::~ContainerBackend() {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
bool ContainerBackend::executeStep(const core::WorkflowStep& step,
|
||||
const QVariantMap& context) {
|
||||
if (m_containerId.isEmpty()) {
|
||||
emit error("Container not prepared");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Execute command in container
|
||||
if (!step.run.isEmpty()) {
|
||||
QProcess process;
|
||||
QStringList args;
|
||||
args << "exec" << m_containerId << "sh" << "-c" << step.run;
|
||||
|
||||
process.start(m_containerRuntime, args);
|
||||
|
||||
if (!process.waitForFinished(300000)) { // 5 minutes
|
||||
emit error("Step execution timeout");
|
||||
return false;
|
||||
}
|
||||
|
||||
QString output = QString::fromUtf8(process.readAllStandardOutput());
|
||||
emit this->output(output);
|
||||
|
||||
if (process.exitCode() != 0) {
|
||||
QString errorMsg = QString::fromUtf8(process.readAllStandardError());
|
||||
emit error("Step failed: " + errorMsg);
|
||||
return false;
|
||||
}
|
||||
} else if (!step.uses.isEmpty()) {
|
||||
// Handle actions like actions/checkout@v3
|
||||
emit this->output("Action execution: " + step.uses + " (stub)");
|
||||
// Would need more complex action resolution
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ContainerBackend::prepareEnvironment(const QString& runsOn) {
|
||||
QString image = mapRunsOnToImage(runsOn);
|
||||
|
||||
QProcess process;
|
||||
QStringList args;
|
||||
args << "run" << "-d" << "-it" << image << "sh";
|
||||
|
||||
process.start(m_containerRuntime, args);
|
||||
|
||||
if (!process.waitForFinished(60000)) {
|
||||
emit error("Container creation timeout");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (process.exitCode() != 0) {
|
||||
QString errorMsg = QString::fromUtf8(process.readAllStandardError());
|
||||
emit error("Failed to create container: " + errorMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_containerId = QString::fromUtf8(process.readAllStandardOutput()).trimmed();
|
||||
return !m_containerId.isEmpty();
|
||||
}
|
||||
|
||||
void ContainerBackend::cleanup() {
|
||||
if (!m_containerId.isEmpty()) {
|
||||
QProcess process;
|
||||
QStringList args;
|
||||
args << "rm" << "-f" << m_containerId;
|
||||
|
||||
process.start(m_containerRuntime, args);
|
||||
process.waitForFinished(30000);
|
||||
|
||||
m_containerId.clear();
|
||||
}
|
||||
}
|
||||
|
||||
bool ContainerBackend::detectRuntime() {
|
||||
// Try docker first
|
||||
QProcess dockerCheck;
|
||||
dockerCheck.start("docker", QStringList() << "--version");
|
||||
|
||||
if (dockerCheck.waitForFinished(5000) && dockerCheck.exitCode() == 0) {
|
||||
m_containerRuntime = "docker";
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try podman
|
||||
QProcess podmanCheck;
|
||||
podmanCheck.start("podman", QStringList() << "--version");
|
||||
|
||||
if (podmanCheck.waitForFinished(5000) && podmanCheck.exitCode() == 0) {
|
||||
m_containerRuntime = "podman";
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QString ContainerBackend::mapRunsOnToImage(const QString& runsOn) const {
|
||||
// Map GitHub runner specs to container images
|
||||
if (runsOn.contains("ubuntu-latest") || runsOn.contains("ubuntu-22.04")) {
|
||||
return "ubuntu:22.04";
|
||||
} else if (runsOn.contains("ubuntu-20.04")) {
|
||||
return "ubuntu:20.04";
|
||||
} else if (runsOn.contains("ubuntu")) {
|
||||
return "ubuntu:latest";
|
||||
} else if (runsOn.contains("debian")) {
|
||||
return "debian:latest";
|
||||
} else if (runsOn.contains("alpine")) {
|
||||
return "alpine:latest";
|
||||
}
|
||||
|
||||
// Default
|
||||
return "ubuntu:22.04";
|
||||
}
|
||||
|
||||
} // namespace backends
|
||||
} // namespace gwt
|
||||
100
src/backends/QemuBackend.cpp
Normal file
100
src/backends/QemuBackend.cpp
Normal file
@@ -0,0 +1,100 @@
|
||||
#include "backends/QemuBackend.h"
|
||||
#include <QProcess>
|
||||
#include <QDebug>
|
||||
|
||||
namespace gwt {
|
||||
namespace backends {
|
||||
|
||||
QemuBackend::QemuBackend(QObject* parent)
|
||||
: ExecutionBackend(parent)
|
||||
{
|
||||
detectQemu();
|
||||
}
|
||||
|
||||
QemuBackend::~QemuBackend() {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
bool QemuBackend::executeStep(const core::WorkflowStep& step,
|
||||
const QVariantMap& context) {
|
||||
if (m_vmId.isEmpty()) {
|
||||
emit error("VM not prepared");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Execute command in VM via SSH or QEMU guest agent
|
||||
if (!step.run.isEmpty()) {
|
||||
// Stub implementation - would need VM communication setup
|
||||
emit output("Executing in VM: " + step.run);
|
||||
// Would use QEMU monitor or SSH to execute
|
||||
} else if (!step.uses.isEmpty()) {
|
||||
emit output("Action execution in VM: " + step.uses + " (stub)");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QemuBackend::prepareEnvironment(const QString& runsOn) {
|
||||
QString vmImage = mapRunsOnToVMImage(runsOn);
|
||||
|
||||
if (!startVM(vmImage)) {
|
||||
emit error("Failed to start VM");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void QemuBackend::cleanup() {
|
||||
stopVM();
|
||||
}
|
||||
|
||||
bool QemuBackend::detectQemu() {
|
||||
QProcess qemuCheck;
|
||||
qemuCheck.start("qemu-system-x86_64", QStringList() << "--version");
|
||||
|
||||
if (qemuCheck.waitForFinished(5000) && qemuCheck.exitCode() == 0) {
|
||||
m_qemuPath = "qemu-system-x86_64";
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QString QemuBackend::mapRunsOnToVMImage(const QString& runsOn) const {
|
||||
// Map GitHub runner specs to VM images
|
||||
// These would be pre-built VM images stored locally
|
||||
if (runsOn.contains("ubuntu-latest") || runsOn.contains("ubuntu-22.04")) {
|
||||
return "ubuntu-22.04.qcow2";
|
||||
} else if (runsOn.contains("ubuntu-20.04")) {
|
||||
return "ubuntu-20.04.qcow2";
|
||||
} else if (runsOn.contains("windows-latest")) {
|
||||
return "windows-2022.qcow2";
|
||||
}
|
||||
|
||||
return "ubuntu-22.04.qcow2";
|
||||
}
|
||||
|
||||
bool QemuBackend::startVM(const QString& imagePath) {
|
||||
// Stub implementation - would need proper VM startup with networking
|
||||
m_vmId = "vm-" + QString::number(QDateTime::currentSecsSinceEpoch());
|
||||
|
||||
// Would execute something like:
|
||||
// qemu-system-x86_64 -m 2048 -smp 2 -hda imagePath -net user -net nic
|
||||
|
||||
emit output("Starting QEMU VM with image: " + imagePath);
|
||||
|
||||
// For now, just simulate
|
||||
return true;
|
||||
}
|
||||
|
||||
void QemuBackend::stopVM() {
|
||||
if (!m_vmId.isEmpty()) {
|
||||
emit output("Stopping VM: " + m_vmId);
|
||||
// Would send shutdown command to VM
|
||||
m_vmId.clear();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace backends
|
||||
} // namespace gwt
|
||||
155
src/cli/CommandHandler.cpp
Normal file
155
src/cli/CommandHandler.cpp
Normal file
@@ -0,0 +1,155 @@
|
||||
#include "cli/CommandHandler.h"
|
||||
#include "core/RepoManager.h"
|
||||
#include "core/JobExecutor.h"
|
||||
#include "core/WorkflowDiscovery.h"
|
||||
#include "core/WorkflowParser.h"
|
||||
#include <QCoreApplication>
|
||||
#include <QTextStream>
|
||||
#include <QDebug>
|
||||
|
||||
namespace gwt {
|
||||
namespace cli {
|
||||
|
||||
CommandHandler::CommandHandler(QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_repoManager(std::make_unique<core::RepoManager>())
|
||||
, m_executor(std::make_unique<core::JobExecutor>())
|
||||
{
|
||||
}
|
||||
|
||||
CommandHandler::~CommandHandler() = default;
|
||||
|
||||
int CommandHandler::execute(const QStringList& args) {
|
||||
if (args.isEmpty() || args[0] == "help" || args[0] == "--help" || args[0] == "-h") {
|
||||
printHelp();
|
||||
return 0;
|
||||
}
|
||||
|
||||
QString command = args[0];
|
||||
|
||||
if (command == "clone") {
|
||||
return handleClone(args.mid(1));
|
||||
} else if (command == "list") {
|
||||
return handleList(args.mid(1));
|
||||
} else if (command == "run") {
|
||||
return handleRun(args.mid(1));
|
||||
} else if (command == "workflows") {
|
||||
return handleWorkflows(args.mid(1));
|
||||
} else {
|
||||
QTextStream err(stderr);
|
||||
err << "Unknown command: " << command << Qt::endl;
|
||||
printHelp();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
void CommandHandler::printHelp() const {
|
||||
QTextStream out(stdout);
|
||||
out << "GithubWorkflowTool - Local GitHub Workflow Simulator" << Qt::endl;
|
||||
out << Qt::endl;
|
||||
out << "Usage: gwt <command> [options]" << Qt::endl;
|
||||
out << Qt::endl;
|
||||
out << "Commands:" << Qt::endl;
|
||||
out << " clone <url> Clone a repository" << Qt::endl;
|
||||
out << " list List cloned repositories" << Qt::endl;
|
||||
out << " workflows <repo> List workflows in a repository" << Qt::endl;
|
||||
out << " run <repo> <wf> Run a workflow" << Qt::endl;
|
||||
out << " help Show this help message" << Qt::endl;
|
||||
out << Qt::endl;
|
||||
}
|
||||
|
||||
int CommandHandler::handleClone(const QStringList& args) {
|
||||
if (args.isEmpty()) {
|
||||
QTextStream err(stderr);
|
||||
err << "Error: Repository URL required" << Qt::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
QString repoUrl = args[0];
|
||||
QTextStream out(stdout);
|
||||
out << "Cloning repository: " << repoUrl << Qt::endl;
|
||||
|
||||
if (m_repoManager->cloneRepository(repoUrl)) {
|
||||
out << "Successfully cloned to: " << m_repoManager->getLocalPath(repoUrl) << Qt::endl;
|
||||
return 0;
|
||||
} else {
|
||||
QTextStream err(stderr);
|
||||
err << "Failed to clone repository" << Qt::endl;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
int CommandHandler::handleList(const QStringList& args) {
|
||||
Q_UNUSED(args);
|
||||
|
||||
QStringList repos = m_repoManager->listRepositories();
|
||||
QTextStream out(stdout);
|
||||
|
||||
out << "Cloned repositories:" << Qt::endl;
|
||||
for (const QString& repo : repos) {
|
||||
out << " " << repo << Qt::endl;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int CommandHandler::handleRun(const QStringList& args) {
|
||||
if (args.size() < 2) {
|
||||
QTextStream err(stderr);
|
||||
err << "Error: Repository path and workflow file required" << Qt::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
QString repoPath = args[0];
|
||||
QString workflowFile = args[1];
|
||||
|
||||
QTextStream out(stdout);
|
||||
out << "Running workflow: " << workflowFile << Qt::endl;
|
||||
|
||||
// Parse workflow
|
||||
core::WorkflowParser parser;
|
||||
core::Workflow workflow = parser.parse(workflowFile);
|
||||
|
||||
if (parser.hasErrors()) {
|
||||
QTextStream err(stderr);
|
||||
err << "Workflow parsing errors:" << Qt::endl;
|
||||
for (const QString& error : parser.getErrors()) {
|
||||
err << " " << error << Qt::endl;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Execute workflow
|
||||
bool useQemu = args.contains("--qemu");
|
||||
if (m_executor->executeWorkflow(workflow, "push", useQemu)) {
|
||||
out << "Workflow execution completed" << Qt::endl;
|
||||
return 0;
|
||||
} else {
|
||||
QTextStream err(stderr);
|
||||
err << "Workflow execution failed" << Qt::endl;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
int CommandHandler::handleWorkflows(const QStringList& args) {
|
||||
if (args.isEmpty()) {
|
||||
QTextStream err(stderr);
|
||||
err << "Error: Repository path required" << Qt::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
QString repoPath = args[0];
|
||||
core::WorkflowDiscovery discovery;
|
||||
QStringList workflows = discovery.discoverWorkflows(repoPath);
|
||||
|
||||
QTextStream out(stdout);
|
||||
out << "Workflows in " << repoPath << ":" << Qt::endl;
|
||||
for (const QString& workflow : workflows) {
|
||||
out << " " << workflow << Qt::endl;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace gwt
|
||||
15
src/cli/main.cpp
Normal file
15
src/cli/main.cpp
Normal file
@@ -0,0 +1,15 @@
|
||||
#include "cli/CommandHandler.h"
|
||||
#include <QCoreApplication>
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
QCoreApplication app(argc, argv);
|
||||
app.setApplicationName("GithubWorkflowTool");
|
||||
app.setApplicationVersion("0.1.0");
|
||||
|
||||
gwt::cli::CommandHandler handler;
|
||||
|
||||
QStringList args = app.arguments();
|
||||
args.removeFirst(); // Remove program name
|
||||
|
||||
return handler.execute(args);
|
||||
}
|
||||
84
src/core/ArtifactManager.cpp
Normal file
84
src/core/ArtifactManager.cpp
Normal file
@@ -0,0 +1,84 @@
|
||||
#include "core/ArtifactManager.h"
|
||||
#include "core/StorageProvider.h"
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QDebug>
|
||||
|
||||
namespace gwt {
|
||||
namespace core {
|
||||
|
||||
ArtifactManager::ArtifactManager(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
ArtifactManager::~ArtifactManager() = default;
|
||||
|
||||
bool ArtifactManager::uploadArtifact(const QString& name,
|
||||
const QString& path,
|
||||
const QString& workflowId) {
|
||||
QString artifactPath = getArtifactPath(name, workflowId);
|
||||
|
||||
QDir().mkpath(QFileInfo(artifactPath).path());
|
||||
|
||||
// Simple file copy for now - would need compression/archiving for directories
|
||||
QFileInfo sourceInfo(path);
|
||||
|
||||
if (sourceInfo.isFile()) {
|
||||
if (!QFile::copy(path, artifactPath)) {
|
||||
emit error("Failed to upload artifact: " + name);
|
||||
return false;
|
||||
}
|
||||
} else if (sourceInfo.isDir()) {
|
||||
// Would need recursive directory copying
|
||||
emit error("Directory artifacts not yet implemented");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ArtifactManager::downloadArtifact(const QString& name,
|
||||
const QString& workflowId,
|
||||
const QString& destinationPath) {
|
||||
QString artifactPath = getArtifactPath(name, workflowId);
|
||||
|
||||
if (!QFile::exists(artifactPath)) {
|
||||
emit error("Artifact not found: " + name);
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir().mkpath(QFileInfo(destinationPath).path());
|
||||
|
||||
if (!QFile::copy(artifactPath, destinationPath)) {
|
||||
emit error("Failed to download artifact: " + name);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QStringList ArtifactManager::listArtifacts(const QString& workflowId) const {
|
||||
QStringList artifacts;
|
||||
|
||||
QString artifactDir = StorageProvider::instance().getCacheRoot() + "/artifacts/" + workflowId;
|
||||
QDir dir(artifactDir);
|
||||
|
||||
if (!dir.exists()) {
|
||||
return artifacts;
|
||||
}
|
||||
|
||||
QFileInfoList files = dir.entryInfoList(QDir::Files);
|
||||
for (const QFileInfo& file : files) {
|
||||
artifacts << file.fileName();
|
||||
}
|
||||
|
||||
return artifacts;
|
||||
}
|
||||
|
||||
QString ArtifactManager::getArtifactPath(const QString& name, const QString& workflowId) const {
|
||||
return StorageProvider::instance().getCacheRoot() + "/artifacts/" + workflowId + "/" + name;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
} // namespace gwt
|
||||
91
src/core/CacheManager.cpp
Normal file
91
src/core/CacheManager.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
#include "core/CacheManager.h"
|
||||
#include "core/StorageProvider.h"
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDebug>
|
||||
|
||||
namespace gwt {
|
||||
namespace core {
|
||||
|
||||
CacheManager::CacheManager(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
CacheManager::~CacheManager() = default;
|
||||
|
||||
bool CacheManager::saveCache(const QString& key, const QStringList& paths) {
|
||||
QString cachePath = getCachePath(key);
|
||||
|
||||
QDir().mkpath(QFileInfo(cachePath).path());
|
||||
|
||||
// Simple implementation - would need tar/compression for real use
|
||||
for (const QString& path : paths) {
|
||||
QFileInfo info(path);
|
||||
if (info.isFile()) {
|
||||
QString destPath = cachePath + "/" + info.fileName();
|
||||
QDir().mkpath(cachePath);
|
||||
if (!QFile::copy(path, destPath)) {
|
||||
emit error("Failed to cache file: " + path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CacheManager::restoreCache(const QString& key, const QStringList& paths) {
|
||||
QString cachePath = getCachePath(key);
|
||||
|
||||
if (!hasCache(key)) {
|
||||
emit cacheMiss(key);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Restore cached files
|
||||
QDir cacheDir(cachePath);
|
||||
QFileInfoList cachedFiles = cacheDir.entryInfoList(QDir::Files);
|
||||
|
||||
for (int i = 0; i < cachedFiles.size() && i < paths.size(); ++i) {
|
||||
QString destPath = paths[i];
|
||||
QDir().mkpath(QFileInfo(destPath).path());
|
||||
|
||||
if (!QFile::copy(cachedFiles[i].filePath(), destPath)) {
|
||||
emit error("Failed to restore cached file: " + destPath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
emit cacheHit(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CacheManager::hasCache(const QString& key) const {
|
||||
QString cachePath = getCachePath(key);
|
||||
return QDir(cachePath).exists();
|
||||
}
|
||||
|
||||
void CacheManager::clearAll() {
|
||||
QString cacheRoot = StorageProvider::instance().getCacheRoot() + "/cache";
|
||||
QDir dir(cacheRoot);
|
||||
dir.removeRecursively();
|
||||
}
|
||||
|
||||
void CacheManager::clearCache(const QString& key) {
|
||||
QString cachePath = getCachePath(key);
|
||||
QDir dir(cachePath);
|
||||
dir.removeRecursively();
|
||||
}
|
||||
|
||||
QString CacheManager::getCachePath(const QString& key) const {
|
||||
// Hash the key for filesystem safety
|
||||
QByteArray hashData = key.toUtf8();
|
||||
QString hash = QString(QCryptographicHash::hash(hashData, QCryptographicHash::Sha256).toHex());
|
||||
|
||||
return StorageProvider::instance().getCacheRoot() + "/cache/" + hash;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
} // namespace gwt
|
||||
144
src/core/JobExecutor.cpp
Normal file
144
src/core/JobExecutor.cpp
Normal file
@@ -0,0 +1,144 @@
|
||||
#include "core/JobExecutor.h"
|
||||
#include "backends/ContainerBackend.h"
|
||||
#include "backends/QemuBackend.h"
|
||||
#include <QDebug>
|
||||
|
||||
namespace gwt {
|
||||
namespace core {
|
||||
|
||||
JobExecutor::JobExecutor(QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_running(false)
|
||||
{
|
||||
}
|
||||
|
||||
JobExecutor::~JobExecutor() = default;
|
||||
|
||||
bool JobExecutor::executeWorkflow(const Workflow& workflow,
|
||||
const QString& triggerEvent,
|
||||
bool useQemu) {
|
||||
if (m_running) {
|
||||
emit error("Execution already in progress");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_running = true;
|
||||
|
||||
// Create appropriate backend
|
||||
if (useQemu) {
|
||||
m_backend = std::make_unique<backends::QemuBackend>(this);
|
||||
} else {
|
||||
m_backend = std::make_unique<backends::ContainerBackend>(this);
|
||||
}
|
||||
|
||||
// Connect backend signals
|
||||
connect(m_backend.get(), &backends::ExecutionBackend::output,
|
||||
[this](const QString& text) {
|
||||
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;
|
||||
}
|
||||
|
||||
emit jobFinished(jobId, true);
|
||||
}
|
||||
|
||||
m_running = false;
|
||||
emit executionFinished(success);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
void JobExecutor::stopExecution() {
|
||||
if (m_running && m_backend) {
|
||||
m_backend->cleanup();
|
||||
m_running = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool JobExecutor::isRunning() const {
|
||||
return m_running;
|
||||
}
|
||||
|
||||
bool JobExecutor::executeJob(const WorkflowJob& job) {
|
||||
// Prepare environment
|
||||
if (!m_backend->prepareEnvironment(job.runsOn)) {
|
||||
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<QString> processed;
|
||||
QMap<QString, QStringList> 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;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
} // namespace gwt
|
||||
107
src/core/MatrixStrategy.cpp
Normal file
107
src/core/MatrixStrategy.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
#include "core/MatrixStrategy.h"
|
||||
#include "core/WorkflowParser.h"
|
||||
#include <QDebug>
|
||||
|
||||
namespace gwt {
|
||||
namespace core {
|
||||
|
||||
MatrixStrategy::MatrixStrategy() = default;
|
||||
|
||||
MatrixStrategy::~MatrixStrategy() = default;
|
||||
|
||||
QList<WorkflowJob> MatrixStrategy::expandMatrix(const WorkflowJob& job) const {
|
||||
QList<WorkflowJob> expandedJobs;
|
||||
|
||||
if (!hasMatrix(job)) {
|
||||
expandedJobs << job;
|
||||
return expandedJobs;
|
||||
}
|
||||
|
||||
// Extract matrix variables
|
||||
QVariantMap matrix = job.strategy["matrix"].toMap();
|
||||
QList<QVariantMap> combinations = generateCombinations(matrix);
|
||||
|
||||
// Create a job for each combination
|
||||
for (const QVariantMap& combo : combinations) {
|
||||
WorkflowJob expandedJob = job;
|
||||
|
||||
// Update job ID to include matrix values
|
||||
QString matrixSuffix = "(";
|
||||
for (auto it = combo.begin(); it != combo.end(); ++it) {
|
||||
if (it != combo.begin()) {
|
||||
matrixSuffix += ", ";
|
||||
}
|
||||
matrixSuffix += it.key() + "=" + it.value().toString();
|
||||
}
|
||||
matrixSuffix += ")";
|
||||
|
||||
expandedJob.id = job.id + matrixSuffix;
|
||||
expandedJob.name = job.name + " " + matrixSuffix;
|
||||
|
||||
// Add matrix variables to environment
|
||||
for (auto it = combo.begin(); it != combo.end(); ++it) {
|
||||
expandedJob.env["matrix." + it.key()] = it.value();
|
||||
}
|
||||
|
||||
expandedJobs << expandedJob;
|
||||
}
|
||||
|
||||
return expandedJobs;
|
||||
}
|
||||
|
||||
bool MatrixStrategy::hasMatrix(const WorkflowJob& job) const {
|
||||
return job.strategy.contains("matrix") && !job.strategy["matrix"].toMap().isEmpty();
|
||||
}
|
||||
|
||||
QList<QVariantMap> MatrixStrategy::generateCombinations(const QVariantMap& matrix) const {
|
||||
QList<QVariantMap> results;
|
||||
|
||||
if (matrix.isEmpty()) {
|
||||
return results;
|
||||
}
|
||||
|
||||
// Get all keys and their values
|
||||
QStringList keys = matrix.keys();
|
||||
QList<QVariantList> valueLists;
|
||||
|
||||
for (const QString& key : keys) {
|
||||
QVariant value = matrix[key];
|
||||
if (value.type() == QVariant::List) {
|
||||
valueLists << value.toList();
|
||||
} else {
|
||||
valueLists << QVariantList{value};
|
||||
}
|
||||
}
|
||||
|
||||
// Generate all combinations
|
||||
QList<int> indices(keys.size(), 0);
|
||||
bool done = false;
|
||||
|
||||
while (!done) {
|
||||
QVariantMap combo;
|
||||
for (int i = 0; i < keys.size(); ++i) {
|
||||
combo[keys[i]] = valueLists[i][indices[i]];
|
||||
}
|
||||
results << combo;
|
||||
|
||||
// Increment indices
|
||||
int pos = keys.size() - 1;
|
||||
while (pos >= 0) {
|
||||
indices[pos]++;
|
||||
if (indices[pos] < valueLists[pos].size()) {
|
||||
break;
|
||||
}
|
||||
indices[pos] = 0;
|
||||
pos--;
|
||||
}
|
||||
|
||||
if (pos < 0) {
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
} // namespace gwt
|
||||
126
src/core/RepoManager.cpp
Normal file
126
src/core/RepoManager.cpp
Normal file
@@ -0,0 +1,126 @@
|
||||
#include "core/RepoManager.h"
|
||||
#include "core/StorageProvider.h"
|
||||
#include <QProcess>
|
||||
#include <QDir>
|
||||
#include <QDebug>
|
||||
|
||||
namespace gwt {
|
||||
namespace core {
|
||||
|
||||
RepoManager::RepoManager(QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_storage(StorageProvider::instance())
|
||||
{
|
||||
}
|
||||
|
||||
RepoManager::~RepoManager() = default;
|
||||
|
||||
bool RepoManager::cloneRepository(const QString& repoUrl, const QString& branch) {
|
||||
QString localPath = m_storage.getRepoDirectory(repoUrl);
|
||||
|
||||
if (isCloned(repoUrl)) {
|
||||
emit error("Repository already cloned at: " + localPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir().mkpath(QFileInfo(localPath).path());
|
||||
|
||||
QProcess git;
|
||||
QStringList args;
|
||||
args << "clone";
|
||||
|
||||
if (!branch.isEmpty()) {
|
||||
args << "--branch" << branch;
|
||||
}
|
||||
|
||||
args << repoUrl << localPath;
|
||||
|
||||
git.start("git", args);
|
||||
|
||||
if (!git.waitForStarted()) {
|
||||
emit error("Failed to start git process");
|
||||
return false;
|
||||
}
|
||||
|
||||
emit cloneProgress(10, "Cloning repository...");
|
||||
|
||||
// Wait for completion with timeout
|
||||
if (!git.waitForFinished(300000)) { // 5 minutes timeout
|
||||
emit error("Git clone timeout");
|
||||
git.kill();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (git.exitCode() != 0) {
|
||||
QString errorMsg = QString::fromUtf8(git.readAllStandardError());
|
||||
emit error("Git clone failed: " + errorMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
emit cloneProgress(100, "Clone completed");
|
||||
emit cloneFinished(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RepoManager::updateRepository(const QString& repoUrl) {
|
||||
QString localPath = getLocalPath(repoUrl);
|
||||
|
||||
if (!isCloned(repoUrl)) {
|
||||
emit error("Repository not cloned: " + repoUrl);
|
||||
return false;
|
||||
}
|
||||
|
||||
QProcess git;
|
||||
git.setWorkingDirectory(localPath);
|
||||
git.start("git", QStringList() << "pull");
|
||||
|
||||
if (!git.waitForFinished(60000)) { // 1 minute timeout
|
||||
emit error("Git pull timeout");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (git.exitCode() != 0) {
|
||||
QString errorMsg = QString::fromUtf8(git.readAllStandardError());
|
||||
emit error("Git pull failed: " + errorMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QString RepoManager::getLocalPath(const QString& repoUrl) const {
|
||||
return m_storage.getRepoDirectory(repoUrl);
|
||||
}
|
||||
|
||||
bool RepoManager::isCloned(const QString& repoUrl) const {
|
||||
QString localPath = getLocalPath(repoUrl);
|
||||
QDir dir(localPath);
|
||||
return dir.exists() && dir.exists(".git");
|
||||
}
|
||||
|
||||
QStringList RepoManager::listRepositories() const {
|
||||
QStringList repos;
|
||||
QString root = m_storage.getRepoStorageRoot();
|
||||
|
||||
QDir rootDir(root);
|
||||
if (!rootDir.exists()) {
|
||||
return repos;
|
||||
}
|
||||
|
||||
// Recursively find all directories with .git
|
||||
QFileInfoList entries = rootDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
for (const QFileInfo& entry : entries) {
|
||||
QDir hostDir(entry.filePath());
|
||||
QFileInfoList repoEntries = hostDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
for (const QFileInfo& repoEntry : repoEntries) {
|
||||
if (QDir(repoEntry.filePath()).exists(".git")) {
|
||||
repos << repoEntry.filePath();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return repos;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
} // namespace gwt
|
||||
101
src/core/StorageProvider.cpp
Normal file
101
src/core/StorageProvider.cpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#include "core/StorageProvider.h"
|
||||
#include <QStandardPaths>
|
||||
#include <QCryptographicHash>
|
||||
#include <QUrl>
|
||||
#include <QDebug>
|
||||
|
||||
namespace gwt {
|
||||
namespace core {
|
||||
|
||||
StorageProvider& StorageProvider::instance() {
|
||||
static StorageProvider instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
StorageProvider::StorageProvider() {
|
||||
#ifdef Q_OS_WIN
|
||||
// Windows: %APPDATA%\GithubWorkflowTool\repos\
|
||||
QString appData = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
||||
m_repoRoot = appData + "/repos";
|
||||
m_cacheRoot = appData + "/cache";
|
||||
#else
|
||||
// Linux: XDG directories
|
||||
QString dataHome = qEnvironmentVariable("XDG_DATA_HOME");
|
||||
if (dataHome.isEmpty()) {
|
||||
dataHome = QDir::homePath() + "/.local/share";
|
||||
}
|
||||
m_repoRoot = dataHome + "/githubworkflowtool/repos";
|
||||
|
||||
QString cacheHome = qEnvironmentVariable("XDG_CACHE_HOME");
|
||||
if (cacheHome.isEmpty()) {
|
||||
cacheHome = QDir::homePath() + "/.cache";
|
||||
}
|
||||
m_cacheRoot = cacheHome + "/githubworkflowtool";
|
||||
#endif
|
||||
|
||||
ensureDirectoriesExist();
|
||||
}
|
||||
|
||||
QString StorageProvider::getRepoStorageRoot() const {
|
||||
return m_repoRoot;
|
||||
}
|
||||
|
||||
QString StorageProvider::getCacheRoot() const {
|
||||
return m_cacheRoot;
|
||||
}
|
||||
|
||||
QString StorageProvider::getRepoDirectory(const QString& repoUrl) const {
|
||||
QString key = generateRepoKey(repoUrl);
|
||||
return m_repoRoot + "/" + key;
|
||||
}
|
||||
|
||||
bool StorageProvider::ensureDirectoriesExist() {
|
||||
QDir dir;
|
||||
bool success = true;
|
||||
|
||||
if (!dir.exists(m_repoRoot)) {
|
||||
success &= dir.mkpath(m_repoRoot);
|
||||
}
|
||||
|
||||
if (!dir.exists(m_cacheRoot)) {
|
||||
success &= dir.mkpath(m_cacheRoot);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
QString StorageProvider::generateRepoKey(const QString& repoUrl) const {
|
||||
QUrl url(repoUrl);
|
||||
QString host = url.host();
|
||||
QString path = url.path();
|
||||
|
||||
// Remove leading/trailing slashes and .git suffix
|
||||
path = path.trimmed();
|
||||
if (path.startsWith('/')) {
|
||||
path = path.mid(1);
|
||||
}
|
||||
if (path.endsWith(".git")) {
|
||||
path = path.left(path.length() - 4);
|
||||
}
|
||||
|
||||
// Generate hash of the full URL for uniqueness
|
||||
QByteArray hashData = repoUrl.toUtf8();
|
||||
QString hash = QString(QCryptographicHash::hash(hashData, QCryptographicHash::Sha256).toHex()).left(8);
|
||||
|
||||
// Create key: host/owner/name_hash
|
||||
QString key = host + "/" + path + "_" + hash;
|
||||
|
||||
// Sanitize the key to be filesystem-safe
|
||||
key.replace(':', '_');
|
||||
key.replace('?', '_');
|
||||
key.replace('*', '_');
|
||||
key.replace('"', '_');
|
||||
key.replace('<', '_');
|
||||
key.replace('>', '_');
|
||||
key.replace('|', '_');
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
} // namespace gwt
|
||||
57
src/core/WorkflowDiscovery.cpp
Normal file
57
src/core/WorkflowDiscovery.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#include "core/WorkflowDiscovery.h"
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QDebug>
|
||||
|
||||
namespace gwt {
|
||||
namespace core {
|
||||
|
||||
WorkflowDiscovery::WorkflowDiscovery(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
WorkflowDiscovery::~WorkflowDiscovery() = default;
|
||||
|
||||
QStringList WorkflowDiscovery::discoverWorkflows(const QString& repoPath) const {
|
||||
QStringList workflows;
|
||||
|
||||
QString workflowDir = repoPath + "/.github/workflows";
|
||||
QDir dir(workflowDir);
|
||||
|
||||
if (!dir.exists()) {
|
||||
return workflows;
|
||||
}
|
||||
|
||||
QStringList filters;
|
||||
filters << "*.yml" << "*.yaml";
|
||||
|
||||
QFileInfoList files = dir.entryInfoList(filters, QDir::Files);
|
||||
for (const QFileInfo& file : files) {
|
||||
if (isValidWorkflow(file.filePath())) {
|
||||
workflows << file.filePath();
|
||||
}
|
||||
}
|
||||
|
||||
return workflows;
|
||||
}
|
||||
|
||||
bool WorkflowDiscovery::hasWorkflows(const QString& repoPath) const {
|
||||
return !discoverWorkflows(repoPath).isEmpty();
|
||||
}
|
||||
|
||||
bool WorkflowDiscovery::isValidWorkflow(const QString& filePath) const {
|
||||
// Basic validation - check if file exists and is readable
|
||||
QFileInfo info(filePath);
|
||||
if (!info.exists() || !info.isFile() || !info.isReadable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// More sophisticated validation would parse the YAML
|
||||
// For now, just check file extension
|
||||
QString suffix = info.suffix().toLower();
|
||||
return suffix == "yml" || suffix == "yaml";
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
} // namespace gwt
|
||||
146
src/core/WorkflowParser.cpp
Normal file
146
src/core/WorkflowParser.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
#include "core/WorkflowParser.h"
|
||||
#include <yaml-cpp/yaml.h>
|
||||
#include <QFile>
|
||||
#include <QDebug>
|
||||
|
||||
namespace gwt {
|
||||
namespace core {
|
||||
|
||||
WorkflowParser::WorkflowParser() = default;
|
||||
|
||||
WorkflowParser::~WorkflowParser() = default;
|
||||
|
||||
Workflow WorkflowParser::parse(const QString& filePath) {
|
||||
m_errors.clear();
|
||||
Workflow workflow;
|
||||
workflow.filePath = filePath;
|
||||
|
||||
try {
|
||||
YAML::Node root = YAML::LoadFile(filePath.toStdString());
|
||||
|
||||
// Parse workflow name
|
||||
if (root["name"]) {
|
||||
workflow.name = QString::fromStdString(root["name"].as<std::string>());
|
||||
}
|
||||
|
||||
// Parse triggers (on)
|
||||
if (root["on"]) {
|
||||
// Simplified parsing - would need more complex handling
|
||||
YAML::Node onNode = root["on"];
|
||||
if (onNode.IsScalar()) {
|
||||
workflow.on["type"] = QString::fromStdString(onNode.as<std::string>());
|
||||
} else if (onNode.IsMap()) {
|
||||
// Store as QVariantMap for now
|
||||
workflow.on["_raw"] = "complex";
|
||||
}
|
||||
}
|
||||
|
||||
// Parse global env
|
||||
if (root["env"]) {
|
||||
YAML::Node envNode = root["env"];
|
||||
for (auto it = envNode.begin(); it != envNode.end(); ++it) {
|
||||
QString key = QString::fromStdString(it->first.as<std::string>());
|
||||
QString value = QString::fromStdString(it->second.as<std::string>());
|
||||
workflow.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse jobs
|
||||
if (root["jobs"]) {
|
||||
YAML::Node jobsNode = root["jobs"];
|
||||
for (auto it = jobsNode.begin(); it != jobsNode.end(); ++it) {
|
||||
QString jobId = QString::fromStdString(it->first.as<std::string>());
|
||||
YAML::Node jobNode = it->second;
|
||||
|
||||
WorkflowJob job;
|
||||
job.id = jobId;
|
||||
|
||||
if (jobNode["name"]) {
|
||||
job.name = QString::fromStdString(jobNode["name"].as<std::string>());
|
||||
}
|
||||
|
||||
if (jobNode["runs-on"]) {
|
||||
job.runsOn = QString::fromStdString(jobNode["runs-on"].as<std::string>());
|
||||
}
|
||||
|
||||
// Parse needs
|
||||
if (jobNode["needs"]) {
|
||||
YAML::Node needsNode = jobNode["needs"];
|
||||
if (needsNode.IsScalar()) {
|
||||
job.needs << QString::fromStdString(needsNode.as<std::string>());
|
||||
} else if (needsNode.IsSequence()) {
|
||||
for (size_t i = 0; i < needsNode.size(); ++i) {
|
||||
job.needs << QString::fromStdString(needsNode[i].as<std::string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse steps
|
||||
if (jobNode["steps"]) {
|
||||
YAML::Node stepsNode = jobNode["steps"];
|
||||
for (size_t i = 0; i < stepsNode.size(); ++i) {
|
||||
YAML::Node stepNode = stepsNode[i];
|
||||
WorkflowStep step;
|
||||
|
||||
if (stepNode["name"]) {
|
||||
step.name = QString::fromStdString(stepNode["name"].as<std::string>());
|
||||
}
|
||||
|
||||
if (stepNode["id"]) {
|
||||
step.id = QString::fromStdString(stepNode["id"].as<std::string>());
|
||||
}
|
||||
|
||||
if (stepNode["run"]) {
|
||||
step.run = QString::fromStdString(stepNode["run"].as<std::string>());
|
||||
}
|
||||
|
||||
if (stepNode["uses"]) {
|
||||
step.uses = QString::fromStdString(stepNode["uses"].as<std::string>());
|
||||
}
|
||||
|
||||
if (stepNode["working-directory"]) {
|
||||
step.workingDirectory = QString::fromStdString(stepNode["working-directory"].as<std::string>());
|
||||
}
|
||||
|
||||
if (stepNode["shell"]) {
|
||||
step.shell = QString::fromStdString(stepNode["shell"].as<std::string>());
|
||||
}
|
||||
|
||||
if (stepNode["if"]) {
|
||||
step.ifCondition = QString::fromStdString(stepNode["if"].as<std::string>());
|
||||
}
|
||||
|
||||
job.steps.append(step);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse strategy (matrix)
|
||||
if (jobNode["strategy"]) {
|
||||
YAML::Node strategyNode = jobNode["strategy"];
|
||||
if (strategyNode["matrix"]) {
|
||||
// Store as QVariantMap for processing by MatrixStrategy
|
||||
job.strategy["matrix"] = "present";
|
||||
}
|
||||
}
|
||||
|
||||
workflow.jobs[jobId] = job;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (const YAML::Exception& e) {
|
||||
m_errors << QString("YAML parsing error: %1").arg(e.what());
|
||||
}
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
bool WorkflowParser::hasErrors() const {
|
||||
return !m_errors.isEmpty();
|
||||
}
|
||||
|
||||
QStringList WorkflowParser::getErrors() const {
|
||||
return m_errors;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
} // namespace gwt
|
||||
29
src/gui/JobView.cpp
Normal file
29
src/gui/JobView.cpp
Normal file
@@ -0,0 +1,29 @@
|
||||
#include "gui/JobView.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QLabel>
|
||||
|
||||
namespace gwt {
|
||||
namespace gui {
|
||||
|
||||
JobView::JobView(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setupUI();
|
||||
}
|
||||
|
||||
JobView::~JobView() = default;
|
||||
|
||||
void JobView::setupUI() {
|
||||
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||
QLabel* label = new QLabel("Job View", this);
|
||||
layout->addWidget(label);
|
||||
}
|
||||
|
||||
void JobView::setJobInfo(const QString& jobId, const QString& status) {
|
||||
Q_UNUSED(jobId);
|
||||
Q_UNUSED(status);
|
||||
// Implementation for job details
|
||||
}
|
||||
|
||||
} // namespace gui
|
||||
} // namespace gwt
|
||||
188
src/gui/MainWindow.cpp
Normal file
188
src/gui/MainWindow.cpp
Normal file
@@ -0,0 +1,188 @@
|
||||
#include "gui/MainWindow.h"
|
||||
#include "core/RepoManager.h"
|
||||
#include "core/JobExecutor.h"
|
||||
#include "core/WorkflowDiscovery.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QTreeWidget>
|
||||
#include <QTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QComboBox>
|
||||
#include <QInputDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QLabel>
|
||||
|
||||
namespace gwt {
|
||||
namespace gui {
|
||||
|
||||
MainWindow::MainWindow(QWidget* parent)
|
||||
: QMainWindow(parent)
|
||||
, m_repoManager(std::make_unique<core::RepoManager>())
|
||||
, m_executor(std::make_unique<core::JobExecutor>())
|
||||
{
|
||||
setupUI();
|
||||
loadRepositories();
|
||||
|
||||
// Connect signals
|
||||
connect(m_executor.get(), &core::JobExecutor::stepOutput,
|
||||
this, &MainWindow::onJobOutput);
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow() = default;
|
||||
|
||||
void MainWindow::setupUI() {
|
||||
setWindowTitle("GitHub Workflow Tool");
|
||||
resize(1000, 700);
|
||||
|
||||
QWidget* central = new QWidget(this);
|
||||
setCentralWidget(central);
|
||||
|
||||
QVBoxLayout* mainLayout = new QVBoxLayout(central);
|
||||
|
||||
// Top buttons
|
||||
QHBoxLayout* buttonLayout = new QHBoxLayout();
|
||||
m_cloneButton = new QPushButton("Clone Repository", this);
|
||||
QPushButton* refreshButton = new QPushButton("Refresh", this);
|
||||
|
||||
buttonLayout->addWidget(m_cloneButton);
|
||||
buttonLayout->addWidget(refreshButton);
|
||||
buttonLayout->addStretch();
|
||||
|
||||
mainLayout->addLayout(buttonLayout);
|
||||
|
||||
// Repository tree
|
||||
QLabel* repoLabel = new QLabel("Repositories:", this);
|
||||
mainLayout->addWidget(repoLabel);
|
||||
|
||||
m_repoTree = new QTreeWidget(this);
|
||||
m_repoTree->setHeaderLabels(QStringList() << "Path");
|
||||
m_repoTree->setMaximumHeight(200);
|
||||
mainLayout->addWidget(m_repoTree);
|
||||
|
||||
// Workflow tree
|
||||
QLabel* workflowLabel = new QLabel("Workflows:", this);
|
||||
mainLayout->addWidget(workflowLabel);
|
||||
|
||||
m_workflowTree = new QTreeWidget(this);
|
||||
m_workflowTree->setHeaderLabels(QStringList() << "Workflow");
|
||||
m_workflowTree->setMaximumHeight(200);
|
||||
mainLayout->addWidget(m_workflowTree);
|
||||
|
||||
// Execution controls
|
||||
QHBoxLayout* execLayout = new QHBoxLayout();
|
||||
m_runButton = new QPushButton("Run Workflow", this);
|
||||
m_backendCombo = new QComboBox(this);
|
||||
m_backendCombo->addItem("Container Backend");
|
||||
m_backendCombo->addItem("QEMU Backend");
|
||||
|
||||
execLayout->addWidget(new QLabel("Backend:", this));
|
||||
execLayout->addWidget(m_backendCombo);
|
||||
execLayout->addWidget(m_runButton);
|
||||
execLayout->addStretch();
|
||||
|
||||
mainLayout->addLayout(execLayout);
|
||||
|
||||
// Output view
|
||||
QLabel* outputLabel = new QLabel("Output:", this);
|
||||
mainLayout->addWidget(outputLabel);
|
||||
|
||||
m_outputView = new QTextEdit(this);
|
||||
m_outputView->setReadOnly(true);
|
||||
mainLayout->addWidget(m_outputView);
|
||||
|
||||
// Connect signals
|
||||
connect(m_cloneButton, &QPushButton::clicked, this, &MainWindow::onCloneRepository);
|
||||
connect(refreshButton, &QPushButton::clicked, this, &MainWindow::onRefreshRepositories);
|
||||
connect(m_repoTree, &QTreeWidget::itemSelectionChanged, this, &MainWindow::onRepositorySelected);
|
||||
connect(m_runButton, &QPushButton::clicked, this, &MainWindow::onRunWorkflow);
|
||||
}
|
||||
|
||||
void MainWindow::loadRepositories() {
|
||||
m_repoTree->clear();
|
||||
QStringList repos = m_repoManager->listRepositories();
|
||||
|
||||
for (const QString& repo : repos) {
|
||||
QTreeWidgetItem* item = new QTreeWidgetItem(m_repoTree);
|
||||
item->setText(0, repo);
|
||||
item->setData(0, Qt::UserRole, repo);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onCloneRepository() {
|
||||
bool ok;
|
||||
QString repoUrl = QInputDialog::getText(this, "Clone Repository",
|
||||
"Repository URL:",
|
||||
QLineEdit::Normal,
|
||||
"https://github.com/user/repo",
|
||||
&ok);
|
||||
|
||||
if (ok && !repoUrl.isEmpty()) {
|
||||
m_outputView->append("Cloning: " + repoUrl);
|
||||
|
||||
if (m_repoManager->cloneRepository(repoUrl)) {
|
||||
m_outputView->append("Successfully cloned");
|
||||
loadRepositories();
|
||||
} else {
|
||||
QMessageBox::warning(this, "Clone Failed", "Failed to clone repository");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onRefreshRepositories() {
|
||||
loadRepositories();
|
||||
}
|
||||
|
||||
void MainWindow::onRepositorySelected() {
|
||||
QList<QTreeWidgetItem*> selected = m_repoTree->selectedItems();
|
||||
if (selected.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString repoPath = selected[0]->data(0, Qt::UserRole).toString();
|
||||
|
||||
// Discover workflows
|
||||
core::WorkflowDiscovery discovery;
|
||||
QStringList workflows = discovery.discoverWorkflows(repoPath);
|
||||
|
||||
m_workflowTree->clear();
|
||||
for (const QString& workflow : workflows) {
|
||||
QTreeWidgetItem* item = new QTreeWidgetItem(m_workflowTree);
|
||||
item->setText(0, QFileInfo(workflow).fileName());
|
||||
item->setData(0, Qt::UserRole, workflow);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onRunWorkflow() {
|
||||
QList<QTreeWidgetItem*> selected = m_workflowTree->selectedItems();
|
||||
if (selected.isEmpty()) {
|
||||
QMessageBox::warning(this, "No Workflow Selected", "Please select a workflow to run");
|
||||
return;
|
||||
}
|
||||
|
||||
QString workflowPath = selected[0]->data(0, Qt::UserRole).toString();
|
||||
m_outputView->append("\n=== Running workflow: " + workflowPath + " ===\n");
|
||||
|
||||
// Parse and execute
|
||||
core::WorkflowParser parser;
|
||||
core::Workflow workflow = parser.parse(workflowPath);
|
||||
|
||||
if (parser.hasErrors()) {
|
||||
m_outputView->append("Parsing errors:");
|
||||
for (const QString& error : parser.getErrors()) {
|
||||
m_outputView->append(" " + error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool useQemu = m_backendCombo->currentIndex() == 1;
|
||||
m_executor->executeWorkflow(workflow, "push", useQemu);
|
||||
}
|
||||
|
||||
void MainWindow::onJobOutput(const QString& jobId, const QString& stepName, const QString& output) {
|
||||
Q_UNUSED(jobId);
|
||||
Q_UNUSED(stepName);
|
||||
m_outputView->append(output);
|
||||
}
|
||||
|
||||
} // namespace gui
|
||||
} // namespace gwt
|
||||
28
src/gui/WorkflowView.cpp
Normal file
28
src/gui/WorkflowView.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#include "gui/WorkflowView.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QLabel>
|
||||
|
||||
namespace gwt {
|
||||
namespace gui {
|
||||
|
||||
WorkflowView::WorkflowView(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setupUI();
|
||||
}
|
||||
|
||||
WorkflowView::~WorkflowView() = default;
|
||||
|
||||
void WorkflowView::setupUI() {
|
||||
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||
QLabel* label = new QLabel("Workflow View", this);
|
||||
layout->addWidget(label);
|
||||
}
|
||||
|
||||
void WorkflowView::loadWorkflow(const QString& workflowPath) {
|
||||
Q_UNUSED(workflowPath);
|
||||
// Implementation for detailed workflow view
|
||||
}
|
||||
|
||||
} // namespace gui
|
||||
} // namespace gwt
|
||||
13
src/gui/main.cpp
Normal file
13
src/gui/main.cpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#include "gui/MainWindow.h"
|
||||
#include <QApplication>
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
QApplication app(argc, argv);
|
||||
app.setApplicationName("GithubWorkflowTool");
|
||||
app.setApplicationVersion("0.1.0");
|
||||
|
||||
gwt::gui::MainWindow window;
|
||||
window.show();
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
Reference in New Issue
Block a user