mirror of
https://github.com/johndoe6345789/GithubWorkflowTool.git
synced 2026-04-24 13:45:02 +00:00
Initial implementation of GithubWorkflowTool
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
37
.gitignore
vendored
37
.gitignore
vendored
@@ -39,3 +39,40 @@
|
||||
|
||||
# debug information files
|
||||
*.dwo
|
||||
|
||||
# Build directories
|
||||
build/
|
||||
build-*/
|
||||
cmake-build-*/
|
||||
|
||||
# Conan
|
||||
conan.lock
|
||||
conaninfo.txt
|
||||
conanbuildinfo.*
|
||||
graph_info.json
|
||||
|
||||
# CMake
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
cmake_install.cmake
|
||||
Makefile
|
||||
*.cmake
|
||||
|
||||
# Qt
|
||||
*.qm
|
||||
*.pro.user
|
||||
*.autosave
|
||||
moc_*.cpp
|
||||
ui_*.h
|
||||
qrc_*.cpp
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
135
BUILD.md
Normal file
135
BUILD.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Building GithubWorkflowTool
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Required Dependencies
|
||||
- CMake 3.22 or higher
|
||||
- Ninja build system
|
||||
- Conan 2.x package manager
|
||||
- Qt 6.6.1 or higher
|
||||
- C++17 compatible compiler (GCC 9+, Clang 10+, MSVC 2019+)
|
||||
- Git
|
||||
|
||||
### Optional Dependencies
|
||||
- Docker or Podman (for container backend)
|
||||
- QEMU (for VM backend)
|
||||
|
||||
## Installing Dependencies
|
||||
|
||||
### Ubuntu/Debian
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install cmake ninja-build python3-pip git
|
||||
pip3 install conan
|
||||
```
|
||||
|
||||
### Windows
|
||||
1. Install CMake from https://cmake.org/download/
|
||||
2. Install Ninja from https://github.com/ninja-build/ninja/releases
|
||||
3. Install Python and pip: https://www.python.org/downloads/
|
||||
4. Install Conan: `pip install conan`
|
||||
5. Install Git: https://git-scm.com/download/win
|
||||
|
||||
## Building the Project
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/johndoe6345789/GithubWorkflowTool.git
|
||||
cd GithubWorkflowTool
|
||||
```
|
||||
|
||||
2. Configure Conan (first time only):
|
||||
```bash
|
||||
conan profile detect --force
|
||||
```
|
||||
|
||||
3. Install dependencies:
|
||||
```bash
|
||||
conan install . --output-folder=build --build=missing
|
||||
```
|
||||
|
||||
4. Configure the project:
|
||||
```bash
|
||||
cmake --preset=default
|
||||
```
|
||||
|
||||
5. Build:
|
||||
```bash
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
## Running the Application
|
||||
|
||||
### CLI
|
||||
```bash
|
||||
./build/gwt --help
|
||||
```
|
||||
|
||||
### GUI
|
||||
```bash
|
||||
./build/gwt-gui
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Clone a repository
|
||||
```bash
|
||||
./build/gwt clone https://github.com/user/repo
|
||||
```
|
||||
|
||||
### List cloned repositories
|
||||
```bash
|
||||
./build/gwt list
|
||||
```
|
||||
|
||||
### Discover workflows
|
||||
```bash
|
||||
./build/gwt workflows /path/to/cloned/repo
|
||||
```
|
||||
|
||||
### Run a workflow
|
||||
```bash
|
||||
./build/gwt run /path/to/cloned/repo /path/to/cloned/repo/.github/workflows/ci.yml
|
||||
```
|
||||
|
||||
### Run with QEMU backend
|
||||
```bash
|
||||
./build/gwt run /path/to/cloned/repo /path/to/cloned/repo/.github/workflows/ci.yml --qemu
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Debug Build
|
||||
```bash
|
||||
conan install . --output-folder=build-debug --build=missing -s build_type=Debug
|
||||
cmake --preset=debug
|
||||
cmake --build build-debug
|
||||
```
|
||||
|
||||
### Clean Build
|
||||
```bash
|
||||
rm -rf build build-debug
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Qt not found
|
||||
Make sure Qt 6 is properly installed and Conan can find it:
|
||||
```bash
|
||||
conan search qt --remote=conancenter
|
||||
```
|
||||
|
||||
### yaml-cpp not found
|
||||
Install yaml-cpp through Conan:
|
||||
```bash
|
||||
conan install yaml-cpp/0.8.0@
|
||||
```
|
||||
|
||||
### Build errors
|
||||
Try cleaning and rebuilding:
|
||||
```bash
|
||||
rm -rf build
|
||||
conan install . --output-folder=build --build=missing
|
||||
cmake --preset=default
|
||||
cmake --build build
|
||||
```
|
||||
73
CMakeLists.txt
Normal file
73
CMakeLists.txt
Normal file
@@ -0,0 +1,73 @@
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
project(GithubWorkflowTool VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
# Conan integration
|
||||
include(${CMAKE_BINARY_DIR}/conan_toolchain.cmake OPTIONAL)
|
||||
|
||||
# Find Qt6
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Widgets)
|
||||
qt_standard_project_setup()
|
||||
|
||||
# Find yaml-cpp for workflow parsing
|
||||
find_package(yaml-cpp REQUIRED)
|
||||
|
||||
# Source files
|
||||
set(CORE_SOURCES
|
||||
src/core/StorageProvider.cpp
|
||||
src/core/RepoManager.cpp
|
||||
src/core/WorkflowDiscovery.cpp
|
||||
src/core/WorkflowParser.cpp
|
||||
src/core/JobExecutor.cpp
|
||||
src/core/MatrixStrategy.cpp
|
||||
src/core/ArtifactManager.cpp
|
||||
src/core/CacheManager.cpp
|
||||
)
|
||||
|
||||
set(BACKEND_SOURCES
|
||||
src/backends/ContainerBackend.cpp
|
||||
src/backends/QemuBackend.cpp
|
||||
)
|
||||
|
||||
set(CLI_SOURCES
|
||||
src/cli/main.cpp
|
||||
src/cli/CommandHandler.cpp
|
||||
)
|
||||
|
||||
set(GUI_SOURCES
|
||||
src/gui/MainWindow.cpp
|
||||
src/gui/WorkflowView.cpp
|
||||
src/gui/JobView.cpp
|
||||
src/gui/main.cpp
|
||||
)
|
||||
|
||||
# Core library
|
||||
add_library(gwt_core STATIC ${CORE_SOURCES} ${BACKEND_SOURCES})
|
||||
target_include_directories(gwt_core PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
target_link_libraries(gwt_core PUBLIC
|
||||
Qt6::Core
|
||||
yaml-cpp
|
||||
)
|
||||
|
||||
# CLI executable
|
||||
add_executable(gwt_cli ${CLI_SOURCES})
|
||||
target_link_libraries(gwt_cli PRIVATE gwt_core Qt6::Core)
|
||||
set_target_properties(gwt_cli PROPERTIES OUTPUT_NAME "gwt")
|
||||
|
||||
# GUI executable
|
||||
add_executable(gwt_gui ${GUI_SOURCES})
|
||||
target_link_libraries(gwt_gui PRIVATE gwt_core Qt6::Core Qt6::Widgets)
|
||||
set_target_properties(gwt_gui PROPERTIES
|
||||
OUTPUT_NAME "gwt-gui"
|
||||
WIN32_EXECUTABLE TRUE
|
||||
)
|
||||
|
||||
# Installation
|
||||
install(TARGETS gwt_cli gwt_gui
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
||||
35
CMakePresets.json
Normal file
35
CMakePresets.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"version": 3,
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "default",
|
||||
"displayName": "Default Config",
|
||||
"description": "Default build using Ninja",
|
||||
"generator": "Ninja",
|
||||
"binaryDir": "${sourceDir}/build",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "debug",
|
||||
"displayName": "Debug Config",
|
||||
"description": "Debug build using Ninja",
|
||||
"generator": "Ninja",
|
||||
"binaryDir": "${sourceDir}/build-debug",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Debug"
|
||||
}
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{
|
||||
"name": "default",
|
||||
"configurePreset": "default"
|
||||
},
|
||||
{
|
||||
"name": "debug",
|
||||
"configurePreset": "debug"
|
||||
}
|
||||
]
|
||||
}
|
||||
117
README.md
117
README.md
@@ -1,2 +1,117 @@
|
||||
# GithubWorkflowTool
|
||||
Clone a repo to APPDATA/XDG and locally simulate its github workflows. It should use C++/QT6/Conan/Ninja/QEmu Github Workflows.
|
||||
|
||||
A cross-platform desktop application that clones Git repositories to per-user cache directories and simulates GitHub Actions workflows locally.
|
||||
|
||||
## Features
|
||||
|
||||
- **Repository Management**: Clone repositories to platform-specific cache directories
|
||||
- Windows: `%APPDATA%\GithubWorkflowTool\repos\`
|
||||
- Linux: `$XDG_DATA_HOME/githubworkflowtool/repos/` or `~/.local/share/githubworkflowtool/repos/`
|
||||
|
||||
- **Workflow Simulation**: Discover and execute GitHub Actions workflows locally
|
||||
- Automatic discovery of `.github/workflows/*.yml` files
|
||||
- Support for jobs, steps, needs, env variables, and outputs
|
||||
- Matrix strategy expansion
|
||||
- Artifacts upload/download (local equivalent)
|
||||
- Caching support (local equivalent of actions/cache)
|
||||
|
||||
- **Multiple Execution Backends**:
|
||||
- **Container Backend**: Fast iteration using Docker or Podman
|
||||
- **QEMU Backend**: Higher fidelity VM-based execution
|
||||
|
||||
- **Dual Interface**:
|
||||
- **CLI**: Command-line interface for automation and scripting
|
||||
- **GUI**: Qt6-based graphical interface for visual inspection and interaction
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- C++17
|
||||
- Qt 6.6+
|
||||
- CMake 3.22+
|
||||
- Conan package manager
|
||||
- Ninja build system
|
||||
- yaml-cpp for workflow parsing
|
||||
- Docker/Podman (optional, for container backend)
|
||||
- QEMU (optional, for VM backend)
|
||||
|
||||
## Quick Start
|
||||
|
||||
See [BUILD.md](BUILD.md) for detailed build instructions.
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
conan install . --output-folder=build --build=missing
|
||||
|
||||
# Build
|
||||
cmake --preset=default
|
||||
cmake --build build
|
||||
|
||||
# Run CLI
|
||||
./build/gwt clone https://github.com/user/repo
|
||||
./build/gwt workflows /path/to/repo
|
||||
./build/gwt run /path/to/repo /path/to/workflow.yml
|
||||
|
||||
# Run GUI
|
||||
./build/gwt-gui
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
- **StorageProvider**: Platform-specific storage path management
|
||||
- **RepoManager**: Git repository cloning and management
|
||||
- **WorkflowDiscovery**: Workflow file discovery
|
||||
- **WorkflowParser**: YAML workflow parsing
|
||||
- **JobExecutor**: Workflow execution orchestration
|
||||
- **MatrixStrategy**: Matrix strategy expansion
|
||||
- **ArtifactManager**: Artifact upload/download
|
||||
- **CacheManager**: Cache management
|
||||
|
||||
### Backends
|
||||
|
||||
- **ContainerBackend**: Executes workflows in Docker/Podman containers
|
||||
- **QemuBackend**: Executes workflows in QEMU virtual machines
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
GithubWorkflowTool/
|
||||
├── CMakeLists.txt # Main CMake configuration
|
||||
├── conanfile.txt # Conan dependencies
|
||||
├── CMakePresets.json # CMake presets
|
||||
├── include/ # Public headers
|
||||
│ ├── core/ # Core functionality headers
|
||||
│ ├── backends/ # Execution backend headers
|
||||
│ ├── cli/ # CLI headers
|
||||
│ └── gui/ # GUI headers
|
||||
├── src/ # Implementation files
|
||||
│ ├── core/ # Core functionality
|
||||
│ ├── backends/ # Execution backends
|
||||
│ ├── cli/ # CLI implementation
|
||||
│ └── gui/ # GUI implementation
|
||||
├── BUILD.md # Build instructions
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
- Windows 11+
|
||||
- Linux (Ubuntu/Debian-based distributions)
|
||||
- macOS (host support; VM images may have limitations)
|
||||
|
||||
## Limitations (v1)
|
||||
|
||||
- Not all GitHub Actions features are supported
|
||||
- Some third-party actions may require container mode or preinstalled tools
|
||||
- Service containers not yet implemented
|
||||
- macOS runner images not supported
|
||||
- Network access in runners may be limited
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please ensure your code follows the existing style and includes appropriate tests.
|
||||
|
||||
17
conanfile.txt
Normal file
17
conanfile.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
[requires]
|
||||
qt/6.6.1
|
||||
yaml-cpp/0.8.0
|
||||
|
||||
[generators]
|
||||
CMakeToolchain
|
||||
CMakeDeps
|
||||
|
||||
[options]
|
||||
qt/*:shared=True
|
||||
qt/*:qttools=True
|
||||
qt/*:qtdeclarative=False
|
||||
qt/*:qtquickcontrols2=False
|
||||
yaml-cpp/*:shared=False
|
||||
|
||||
[layout]
|
||||
cmake_layout
|
||||
41
include/backends/ContainerBackend.h
Normal file
41
include/backends/ContainerBackend.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include "ExecutionBackend.h"
|
||||
|
||||
namespace gwt {
|
||||
namespace backends {
|
||||
|
||||
/**
|
||||
* @brief Container-based execution backend (using Docker or Podman)
|
||||
*/
|
||||
class ContainerBackend : public ExecutionBackend {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ContainerBackend(QObject* parent = nullptr);
|
||||
~ContainerBackend() override;
|
||||
|
||||
bool executeStep(const core::WorkflowStep& step,
|
||||
const QVariantMap& context) override;
|
||||
|
||||
bool prepareEnvironment(const QString& runsOn) override;
|
||||
|
||||
void cleanup() override;
|
||||
|
||||
private:
|
||||
QString m_containerId;
|
||||
QString m_containerRuntime; // "docker" or "podman"
|
||||
|
||||
/**
|
||||
* @brief Detect available container runtime
|
||||
*/
|
||||
bool detectRuntime();
|
||||
|
||||
/**
|
||||
* @brief Map GitHub runner spec to container image
|
||||
*/
|
||||
QString mapRunsOnToImage(const QString& runsOn) const;
|
||||
};
|
||||
|
||||
} // namespace backends
|
||||
} // namespace gwt
|
||||
47
include/backends/ExecutionBackend.h
Normal file
47
include/backends/ExecutionBackend.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/WorkflowParser.h"
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
namespace gwt {
|
||||
namespace backends {
|
||||
|
||||
/**
|
||||
* @brief Base class for execution backends
|
||||
*/
|
||||
class ExecutionBackend : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ExecutionBackend(QObject* parent = nullptr);
|
||||
~ExecutionBackend() override;
|
||||
|
||||
/**
|
||||
* @brief Execute a job step
|
||||
* @param step The step to execute
|
||||
* @param context Execution context (env vars, working dir, etc.)
|
||||
* @return true if successful
|
||||
*/
|
||||
virtual bool executeStep(const core::WorkflowStep& step,
|
||||
const QVariantMap& context) = 0;
|
||||
|
||||
/**
|
||||
* @brief Prepare the execution environment
|
||||
* @param runsOn The runner specification (ubuntu-latest, etc.)
|
||||
* @return true if successful
|
||||
*/
|
||||
virtual bool prepareEnvironment(const QString& runsOn) = 0;
|
||||
|
||||
/**
|
||||
* @brief Cleanup the execution environment
|
||||
*/
|
||||
virtual void cleanup() = 0;
|
||||
|
||||
signals:
|
||||
void output(const QString& text);
|
||||
void error(const QString& errorMessage);
|
||||
};
|
||||
|
||||
} // namespace backends
|
||||
} // namespace gwt
|
||||
51
include/backends/QemuBackend.h
Normal file
51
include/backends/QemuBackend.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include "ExecutionBackend.h"
|
||||
|
||||
namespace gwt {
|
||||
namespace backends {
|
||||
|
||||
/**
|
||||
* @brief QEMU VM-based execution backend for higher fidelity
|
||||
*/
|
||||
class QemuBackend : public ExecutionBackend {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit QemuBackend(QObject* parent = nullptr);
|
||||
~QemuBackend() override;
|
||||
|
||||
bool executeStep(const core::WorkflowStep& step,
|
||||
const QVariantMap& context) override;
|
||||
|
||||
bool prepareEnvironment(const QString& runsOn) override;
|
||||
|
||||
void cleanup() override;
|
||||
|
||||
private:
|
||||
QString m_vmId;
|
||||
QString m_qemuPath;
|
||||
|
||||
/**
|
||||
* @brief Find QEMU executable
|
||||
*/
|
||||
bool detectQemu();
|
||||
|
||||
/**
|
||||
* @brief Map GitHub runner spec to VM image
|
||||
*/
|
||||
QString mapRunsOnToVMImage(const QString& runsOn) const;
|
||||
|
||||
/**
|
||||
* @brief Start the VM
|
||||
*/
|
||||
bool startVM(const QString& imagePath);
|
||||
|
||||
/**
|
||||
* @brief Stop the VM
|
||||
*/
|
||||
void stopVM();
|
||||
};
|
||||
|
||||
} // namespace backends
|
||||
} // namespace gwt
|
||||
44
include/cli/CommandHandler.h
Normal file
44
include/cli/CommandHandler.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
#include <memory>
|
||||
|
||||
namespace gwt {
|
||||
namespace core {
|
||||
class RepoManager;
|
||||
class JobExecutor;
|
||||
}
|
||||
|
||||
namespace cli {
|
||||
|
||||
/**
|
||||
* @brief Handles command-line interface commands
|
||||
*/
|
||||
class CommandHandler : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CommandHandler(QObject* parent = nullptr);
|
||||
~CommandHandler() override;
|
||||
|
||||
/**
|
||||
* @brief Execute a command with arguments
|
||||
* @param args Command line arguments
|
||||
* @return Exit code
|
||||
*/
|
||||
int execute(const QStringList& args);
|
||||
|
||||
private:
|
||||
std::unique_ptr<core::RepoManager> m_repoManager;
|
||||
std::unique_ptr<core::JobExecutor> m_executor;
|
||||
|
||||
void printHelp() const;
|
||||
int handleClone(const QStringList& args);
|
||||
int handleList(const QStringList& args);
|
||||
int handleRun(const QStringList& args);
|
||||
int handleWorkflows(const QStringList& args);
|
||||
};
|
||||
|
||||
} // namespace cli
|
||||
} // namespace gwt
|
||||
58
include/core/ArtifactManager.h
Normal file
58
include/core/ArtifactManager.h
Normal file
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QObject>
|
||||
|
||||
namespace gwt {
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief Manages workflow artifacts (upload/download)
|
||||
*/
|
||||
class ArtifactManager : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ArtifactManager(QObject* parent = nullptr);
|
||||
~ArtifactManager() override;
|
||||
|
||||
/**
|
||||
* @brief Upload an artifact
|
||||
* @param name Artifact name
|
||||
* @param path Path to artifact file or directory
|
||||
* @param workflowId Associated workflow ID
|
||||
* @return true if successful
|
||||
*/
|
||||
bool uploadArtifact(const QString& name,
|
||||
const QString& path,
|
||||
const QString& workflowId);
|
||||
|
||||
/**
|
||||
* @brief Download an artifact
|
||||
* @param name Artifact name
|
||||
* @param workflowId Associated workflow ID
|
||||
* @param destinationPath Where to download the artifact
|
||||
* @return true if successful
|
||||
*/
|
||||
bool downloadArtifact(const QString& name,
|
||||
const QString& workflowId,
|
||||
const QString& destinationPath);
|
||||
|
||||
/**
|
||||
* @brief List all artifacts for a workflow
|
||||
* @param workflowId The workflow ID
|
||||
* @return List of artifact names
|
||||
*/
|
||||
QStringList listArtifacts(const QString& workflowId) const;
|
||||
|
||||
signals:
|
||||
void uploadProgress(int percentage);
|
||||
void downloadProgress(int percentage);
|
||||
void error(const QString& errorMessage);
|
||||
|
||||
private:
|
||||
QString getArtifactPath(const QString& name, const QString& workflowId) const;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
} // namespace gwt
|
||||
64
include/core/CacheManager.h
Normal file
64
include/core/CacheManager.h
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QObject>
|
||||
|
||||
namespace gwt {
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief Manages workflow caching (actions/cache equivalent)
|
||||
*/
|
||||
class CacheManager : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CacheManager(QObject* parent = nullptr);
|
||||
~CacheManager() override;
|
||||
|
||||
/**
|
||||
* @brief Save paths to cache with a key
|
||||
* @param key Cache key
|
||||
* @param paths Paths to cache
|
||||
* @return true if successful
|
||||
*/
|
||||
bool saveCache(const QString& key, const QStringList& paths);
|
||||
|
||||
/**
|
||||
* @brief Restore cached paths
|
||||
* @param key Cache key
|
||||
* @param paths Paths to restore to
|
||||
* @return true if cache hit and restored
|
||||
*/
|
||||
bool restoreCache(const QString& key, const QStringList& paths);
|
||||
|
||||
/**
|
||||
* @brief Check if a cache key exists
|
||||
* @param key Cache key
|
||||
* @return true if exists
|
||||
*/
|
||||
bool hasCache(const QString& key) const;
|
||||
|
||||
/**
|
||||
* @brief Clear all caches
|
||||
*/
|
||||
void clearAll();
|
||||
|
||||
/**
|
||||
* @brief Clear specific cache entry
|
||||
* @param key Cache key
|
||||
*/
|
||||
void clearCache(const QString& key);
|
||||
|
||||
signals:
|
||||
void cacheHit(const QString& key);
|
||||
void cacheMiss(const QString& key);
|
||||
void error(const QString& errorMessage);
|
||||
|
||||
private:
|
||||
QString getCachePath(const QString& key) const;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
} // namespace gwt
|
||||
71
include/core/JobExecutor.h
Normal file
71
include/core/JobExecutor.h
Normal file
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
#include "WorkflowParser.h"
|
||||
#include <QObject>
|
||||
#include <memory>
|
||||
|
||||
namespace gwt {
|
||||
namespace backends {
|
||||
class ExecutionBackend;
|
||||
}
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief Executes workflow jobs and manages their lifecycle
|
||||
*/
|
||||
class JobExecutor : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit JobExecutor(QObject* parent = nullptr);
|
||||
~JobExecutor() override;
|
||||
|
||||
/**
|
||||
* @brief Execute a complete workflow
|
||||
* @param workflow The workflow to execute
|
||||
* @param triggerEvent The event that triggered the workflow
|
||||
* @param useQemu Use QEMU backend instead of container backend
|
||||
* @return true if execution started successfully
|
||||
*/
|
||||
bool executeWorkflow(const Workflow& workflow,
|
||||
const QString& triggerEvent,
|
||||
bool useQemu = false);
|
||||
|
||||
/**
|
||||
* @brief Stop execution of current workflow
|
||||
*/
|
||||
void stopExecution();
|
||||
|
||||
/**
|
||||
* @brief Check if execution is currently running
|
||||
* @return true if running
|
||||
*/
|
||||
bool isRunning() const;
|
||||
|
||||
signals:
|
||||
void jobStarted(const QString& jobId);
|
||||
void jobFinished(const QString& jobId, bool success);
|
||||
void stepStarted(const QString& jobId, const QString& stepName);
|
||||
void stepFinished(const QString& jobId, const QString& stepName, bool success);
|
||||
void stepOutput(const QString& jobId, const QString& stepName, const QString& output);
|
||||
void executionFinished(bool success);
|
||||
void error(const QString& errorMessage);
|
||||
|
||||
private:
|
||||
bool m_running;
|
||||
std::unique_ptr<backends::ExecutionBackend> m_backend;
|
||||
|
||||
/**
|
||||
* @brief Execute a single job
|
||||
*/
|
||||
bool executeJob(const WorkflowJob& job);
|
||||
|
||||
/**
|
||||
* @brief Resolve job dependencies (needs)
|
||||
*/
|
||||
QStringList resolveJobOrder(const Workflow& workflow) const;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
} // namespace gwt
|
||||
41
include/core/MatrixStrategy.h
Normal file
41
include/core/MatrixStrategy.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <QVariantMap>
|
||||
#include <QList>
|
||||
|
||||
namespace gwt {
|
||||
namespace core {
|
||||
|
||||
struct WorkflowJob;
|
||||
|
||||
/**
|
||||
* @brief Handles matrix strategy expansion for workflow jobs
|
||||
*/
|
||||
class MatrixStrategy {
|
||||
public:
|
||||
MatrixStrategy();
|
||||
~MatrixStrategy();
|
||||
|
||||
/**
|
||||
* @brief Expand a job with matrix strategy into multiple jobs
|
||||
* @param job The job with matrix strategy
|
||||
* @return List of expanded jobs
|
||||
*/
|
||||
QList<WorkflowJob> expandMatrix(const WorkflowJob& job) const;
|
||||
|
||||
/**
|
||||
* @brief Check if a job has a matrix strategy
|
||||
* @param job The job to check
|
||||
* @return true if matrix strategy is present
|
||||
*/
|
||||
bool hasMatrix(const WorkflowJob& job) const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Generate all combinations from matrix variables
|
||||
*/
|
||||
QList<QVariantMap> generateCombinations(const QVariantMap& matrix) const;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
} // namespace gwt
|
||||
68
include/core/RepoManager.h
Normal file
68
include/core/RepoManager.h
Normal file
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QObject>
|
||||
#include <memory>
|
||||
|
||||
namespace gwt {
|
||||
namespace core {
|
||||
|
||||
class StorageProvider;
|
||||
|
||||
/**
|
||||
* @brief Manages Git repository cloning and operations
|
||||
*/
|
||||
class RepoManager : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit RepoManager(QObject* parent = nullptr);
|
||||
~RepoManager() override;
|
||||
|
||||
/**
|
||||
* @brief Clone a repository to the local storage
|
||||
* @param repoUrl The repository URL (e.g., https://github.com/owner/repo)
|
||||
* @param branch Optional branch to clone (default: main/master)
|
||||
* @return true if successful
|
||||
*/
|
||||
bool cloneRepository(const QString& repoUrl, const QString& branch = QString());
|
||||
|
||||
/**
|
||||
* @brief Update an existing repository
|
||||
* @param repoUrl The repository URL
|
||||
* @return true if successful
|
||||
*/
|
||||
bool updateRepository(const QString& repoUrl);
|
||||
|
||||
/**
|
||||
* @brief Get the local path for a repository
|
||||
* @param repoUrl The repository URL
|
||||
* @return Local file system path
|
||||
*/
|
||||
QString getLocalPath(const QString& repoUrl) const;
|
||||
|
||||
/**
|
||||
* @brief Check if a repository is already cloned
|
||||
* @param repoUrl The repository URL
|
||||
* @return true if already cloned
|
||||
*/
|
||||
bool isCloned(const QString& repoUrl) const;
|
||||
|
||||
/**
|
||||
* @brief List all cloned repositories
|
||||
* @return List of repository URLs
|
||||
*/
|
||||
QStringList listRepositories() const;
|
||||
|
||||
signals:
|
||||
void cloneProgress(int percentage, const QString& message);
|
||||
void cloneFinished(bool success);
|
||||
void error(const QString& errorMessage);
|
||||
|
||||
private:
|
||||
StorageProvider& m_storage;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
} // namespace gwt
|
||||
66
include/core/StorageProvider.h
Normal file
66
include/core/StorageProvider.h
Normal file
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QDir>
|
||||
|
||||
namespace gwt {
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief Provides platform-specific storage paths for repository management
|
||||
*
|
||||
* Windows: %APPDATA%\GithubWorkflowTool\repos\
|
||||
* Linux: $XDG_DATA_HOME/githubworkflowtool/repos/ or ~/.local/share/githubworkflowtool/repos/
|
||||
* Cache: $XDG_CACHE_HOME/githubworkflowtool/ or ~/.cache/githubworkflowtool/
|
||||
*/
|
||||
class StorageProvider {
|
||||
public:
|
||||
/**
|
||||
* @brief Get the singleton instance
|
||||
*/
|
||||
static StorageProvider& instance();
|
||||
|
||||
/**
|
||||
* @brief Get the root directory for repository storage
|
||||
* @return Path to repository storage root
|
||||
*/
|
||||
QString getRepoStorageRoot() const;
|
||||
|
||||
/**
|
||||
* @brief Get the cache directory
|
||||
* @return Path to cache directory
|
||||
*/
|
||||
QString getCacheRoot() const;
|
||||
|
||||
/**
|
||||
* @brief Get the directory for a specific repository
|
||||
* @param repoUrl The repository URL
|
||||
* @return Path to the repository's local storage
|
||||
*/
|
||||
QString getRepoDirectory(const QString& repoUrl) const;
|
||||
|
||||
/**
|
||||
* @brief Ensure storage directories exist
|
||||
* @return true if directories were created or already exist
|
||||
*/
|
||||
bool ensureDirectoriesExist();
|
||||
|
||||
private:
|
||||
StorageProvider();
|
||||
~StorageProvider() = default;
|
||||
StorageProvider(const StorageProvider&) = delete;
|
||||
StorageProvider& operator=(const StorageProvider&) = delete;
|
||||
|
||||
/**
|
||||
* @brief Generate a normalized repository key from URL
|
||||
* @param repoUrl The repository URL
|
||||
* @return Normalized key (host/owner/name + hash)
|
||||
*/
|
||||
QString generateRepoKey(const QString& repoUrl) const;
|
||||
|
||||
QString m_repoRoot;
|
||||
QString m_cacheRoot;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
} // namespace gwt
|
||||
44
include/core/WorkflowDiscovery.h
Normal file
44
include/core/WorkflowDiscovery.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QObject>
|
||||
|
||||
namespace gwt {
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief Discovers GitHub workflow files in a repository
|
||||
*/
|
||||
class WorkflowDiscovery : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WorkflowDiscovery(QObject* parent = nullptr);
|
||||
~WorkflowDiscovery() override;
|
||||
|
||||
/**
|
||||
* @brief Discover workflow files in a repository
|
||||
* @param repoPath Local path to the repository
|
||||
* @return List of workflow file paths
|
||||
*/
|
||||
QStringList discoverWorkflows(const QString& repoPath) const;
|
||||
|
||||
/**
|
||||
* @brief Check if a path contains valid workflow files
|
||||
* @param repoPath Local path to check
|
||||
* @return true if workflows exist
|
||||
*/
|
||||
bool hasWorkflows(const QString& repoPath) const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Validate that a file is a proper YAML workflow
|
||||
* @param filePath Path to the YAML file
|
||||
* @return true if valid
|
||||
*/
|
||||
bool isValidWorkflow(const QString& filePath) const;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
} // namespace gwt
|
||||
84
include/core/WorkflowParser.h
Normal file
84
include/core/WorkflowParser.h
Normal file
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QVariantMap>
|
||||
#include <QStringList>
|
||||
#include <memory>
|
||||
|
||||
namespace gwt {
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief Represents a workflow step
|
||||
*/
|
||||
struct WorkflowStep {
|
||||
QString name;
|
||||
QString id;
|
||||
QString run; // Shell command
|
||||
QString uses; // Action to use (e.g., actions/checkout@v3)
|
||||
QVariantMap with; // Parameters for the action
|
||||
QVariantMap env; // Environment variables
|
||||
QString workingDirectory;
|
||||
QString shell;
|
||||
QString ifCondition; // Conditional execution
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Represents a workflow job
|
||||
*/
|
||||
struct WorkflowJob {
|
||||
QString id;
|
||||
QString name;
|
||||
QString runsOn; // e.g., ubuntu-latest, windows-latest
|
||||
QStringList needs; // Dependencies on other jobs
|
||||
QVariantMap env; // Environment variables
|
||||
QVariantMap outputs; // Job outputs
|
||||
QList<WorkflowStep> steps;
|
||||
QVariantMap strategy; // Matrix strategy
|
||||
QString ifCondition; // Conditional execution
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Represents a complete workflow
|
||||
*/
|
||||
struct Workflow {
|
||||
QString name;
|
||||
QString filePath;
|
||||
QVariantMap on; // Trigger events
|
||||
QVariantMap env; // Global environment variables
|
||||
QMap<QString, WorkflowJob> jobs;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Parses GitHub workflow YAML files
|
||||
*/
|
||||
class WorkflowParser {
|
||||
public:
|
||||
WorkflowParser();
|
||||
~WorkflowParser();
|
||||
|
||||
/**
|
||||
* @brief Parse a workflow file
|
||||
* @param filePath Path to the workflow YAML file
|
||||
* @return Parsed workflow structure
|
||||
*/
|
||||
Workflow parse(const QString& filePath);
|
||||
|
||||
/**
|
||||
* @brief Check if the last parse had errors
|
||||
* @return true if errors occurred
|
||||
*/
|
||||
bool hasErrors() const;
|
||||
|
||||
/**
|
||||
* @brief Get error messages from last parse
|
||||
* @return List of error messages
|
||||
*/
|
||||
QStringList getErrors() const;
|
||||
|
||||
private:
|
||||
QStringList m_errors;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
} // namespace gwt
|
||||
25
include/gui/JobView.h
Normal file
25
include/gui/JobView.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
namespace gwt {
|
||||
namespace gui {
|
||||
|
||||
/**
|
||||
* @brief Widget for displaying job execution details
|
||||
*/
|
||||
class JobView : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit JobView(QWidget* parent = nullptr);
|
||||
~JobView() override;
|
||||
|
||||
void setJobInfo(const QString& jobId, const QString& status);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
};
|
||||
|
||||
} // namespace gui
|
||||
} // namespace gwt
|
||||
52
include/gui/MainWindow.h
Normal file
52
include/gui/MainWindow.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <memory>
|
||||
|
||||
class QTreeWidget;
|
||||
class QTextEdit;
|
||||
class QPushButton;
|
||||
class QComboBox;
|
||||
|
||||
namespace gwt {
|
||||
namespace core {
|
||||
class RepoManager;
|
||||
class JobExecutor;
|
||||
}
|
||||
|
||||
namespace gui {
|
||||
|
||||
/**
|
||||
* @brief Main window for the GUI application
|
||||
*/
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainWindow(QWidget* parent = nullptr);
|
||||
~MainWindow() override;
|
||||
|
||||
private slots:
|
||||
void onCloneRepository();
|
||||
void onRefreshRepositories();
|
||||
void onRepositorySelected();
|
||||
void onRunWorkflow();
|
||||
void onJobOutput(const QString& jobId, const QString& stepName, const QString& output);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void loadRepositories();
|
||||
|
||||
QTreeWidget* m_repoTree;
|
||||
QTreeWidget* m_workflowTree;
|
||||
QTextEdit* m_outputView;
|
||||
QPushButton* m_cloneButton;
|
||||
QPushButton* m_runButton;
|
||||
QComboBox* m_backendCombo;
|
||||
|
||||
std::unique_ptr<core::RepoManager> m_repoManager;
|
||||
std::unique_ptr<core::JobExecutor> m_executor;
|
||||
};
|
||||
|
||||
} // namespace gui
|
||||
} // namespace gwt
|
||||
25
include/gui/WorkflowView.h
Normal file
25
include/gui/WorkflowView.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
namespace gwt {
|
||||
namespace gui {
|
||||
|
||||
/**
|
||||
* @brief Widget for displaying workflow information
|
||||
*/
|
||||
class WorkflowView : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WorkflowView(QWidget* parent = nullptr);
|
||||
~WorkflowView() override;
|
||||
|
||||
void loadWorkflow(const QString& workflowPath);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
};
|
||||
|
||||
} // namespace gui
|
||||
} // namespace gwt
|
||||
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