Initial implementation of GithubWorkflowTool

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-27 03:03:18 +00:00
parent 7121ad38d3
commit 9b992001f3
37 changed files with 2711 additions and 1 deletions

View 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

View 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
View 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
View 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);
}

View 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
View 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
View 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
View 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
View 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

View 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

View 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
View 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
View 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
View 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
View 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
View 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();
}