mirror of
https://github.com/bahdotsh/wrkflw.git
synced 2026-01-03 02:46:18 +01:00
Compare commits
16 Commits
feature/se
...
wrkflw-exe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5051f71b8b | ||
|
|
64b980d254 | ||
|
|
2d809388a2 | ||
|
|
03af6cb7c1 | ||
|
|
ae52779e11 | ||
|
|
fe7be3e1ae | ||
|
|
30f405ccb9 | ||
|
|
1d56d86ba5 | ||
|
|
f1ca411281 | ||
|
|
797e31e3d3 | ||
|
|
4e66f65de7 | ||
|
|
335886ac70 | ||
|
|
8005cbb7ee | ||
|
|
5b216f59e6 | ||
|
|
7a17d26589 | ||
|
|
6efad9ce96 |
975
Cargo.lock
generated
975
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ members = [
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
edition = "2021"
|
||||
description = "A GitHub Actions workflow validator and executor"
|
||||
documentation = "https://github.com/bahdotsh/wrkflw"
|
||||
@@ -44,7 +44,7 @@ rayon = "1.7.0"
|
||||
num_cpus = "1.16.0"
|
||||
regex = "1.10"
|
||||
lazy_static = "1.4"
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "json"] }
|
||||
libc = "0.2"
|
||||
nix = { version = "0.27.1", features = ["fs"] }
|
||||
urlencoding = "2.1.3"
|
||||
|
||||
@@ -21,26 +21,9 @@ pub fn evaluate_workflow_file(path: &Path, verbose: bool) -> Result<ValidationRe
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// Check if name exists
|
||||
if workflow.get("name").is_none() {
|
||||
// Check if this might be a reusable workflow caller before reporting missing name
|
||||
let has_reusable_workflow_job = if let Some(Value::Mapping(jobs)) = workflow.get("jobs") {
|
||||
jobs.values().any(|job| {
|
||||
if let Some(job_config) = job.as_mapping() {
|
||||
job_config.contains_key(Value::String("uses".to_string()))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// Only report missing name if it's not a workflow with reusable workflow jobs
|
||||
if !has_reusable_workflow_job {
|
||||
result.add_issue("Workflow is missing a name".to_string());
|
||||
}
|
||||
}
|
||||
// Note: The 'name' field is optional per GitHub Actions specification.
|
||||
// When omitted, GitHub displays the workflow file path relative to the repository root.
|
||||
// We do not validate name presence as it's not required by the schema.
|
||||
|
||||
// Check if jobs section exists
|
||||
match workflow.get("jobs") {
|
||||
|
||||
@@ -27,6 +27,7 @@ chrono.workspace = true
|
||||
dirs.workspace = true
|
||||
futures.workspace = true
|
||||
futures-util.workspace = true
|
||||
ignore = "0.4"
|
||||
lazy_static.workspace = true
|
||||
num_cpus.workspace = true
|
||||
once_cell.workspace = true
|
||||
|
||||
@@ -9,6 +9,8 @@ use std::path::Path;
|
||||
use std::process::Command;
|
||||
use thiserror::Error;
|
||||
|
||||
use ignore::{gitignore::GitignoreBuilder, Match};
|
||||
|
||||
use crate::dependency;
|
||||
use crate::docker;
|
||||
use crate::environment;
|
||||
@@ -568,7 +570,7 @@ async fn prepare_action(
|
||||
} else {
|
||||
// It's a JavaScript or composite action
|
||||
// For simplicity, we'll use node to run it (this would need more work for full support)
|
||||
return Ok("node:16-buster-slim".to_string());
|
||||
return Ok("node:20-slim".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -626,7 +628,7 @@ fn determine_action_image(repository: &str) -> String {
|
||||
{
|
||||
"catthehacker/ubuntu:act-latest".to_string() // Use act runner image for core actions
|
||||
} else {
|
||||
"node:16-buster-slim".to_string() // Default for other actions
|
||||
"node:20-slim".to_string() // Default for other actions
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -804,13 +806,6 @@ async fn execute_job(ctx: JobExecutionContext<'_>) -> Result<JobResult, Executio
|
||||
ExecutionError::Execution(format!("Failed to get current directory: {}", e))
|
||||
})?;
|
||||
|
||||
// Copy project files to the job workspace directory
|
||||
wrkflw_logging::info(&format!(
|
||||
"Copying project files to job workspace: {}",
|
||||
job_dir.path().display()
|
||||
));
|
||||
copy_directory_contents(¤t_dir, job_dir.path())?;
|
||||
|
||||
wrkflw_logging::info(&format!("Executing job: {}", ctx.job_name));
|
||||
|
||||
let mut job_success = true;
|
||||
@@ -1010,13 +1005,6 @@ async fn execute_matrix_job(
|
||||
ExecutionError::Execution(format!("Failed to get current directory: {}", e))
|
||||
})?;
|
||||
|
||||
// Copy project files to the job workspace directory
|
||||
wrkflw_logging::info(&format!(
|
||||
"Copying project files to job workspace: {}",
|
||||
job_dir.path().display()
|
||||
));
|
||||
copy_directory_contents(¤t_dir, job_dir.path())?;
|
||||
|
||||
let job_success = if job_template.steps.is_empty() {
|
||||
wrkflw_logging::warning(&format!("Job '{}' has no steps", matrix_job_name));
|
||||
true
|
||||
@@ -1172,28 +1160,13 @@ async fn execute_step(ctx: StepExecutionContext<'_>) -> Result<StepResult, Execu
|
||||
detailed_output
|
||||
.push_str(&format!(" - Destination: {}\n", ctx.working_dir.display()));
|
||||
|
||||
// Add list of top-level files/directories that were copied (limit to 10)
|
||||
detailed_output.push_str("\nTop-level files/directories copied:\n");
|
||||
// Add a summary count instead of listing all files
|
||||
if let Ok(entries) = std::fs::read_dir(¤t_dir) {
|
||||
for (i, entry) in entries.take(10).enumerate() {
|
||||
if let Ok(entry) = entry {
|
||||
let file_type = if entry.path().is_dir() {
|
||||
"directory"
|
||||
} else {
|
||||
"file"
|
||||
};
|
||||
detailed_output.push_str(&format!(
|
||||
" - {} ({})\n",
|
||||
entry.file_name().to_string_lossy(),
|
||||
file_type
|
||||
));
|
||||
}
|
||||
|
||||
if i >= 9 {
|
||||
detailed_output.push_str(" - ... (more items not shown)\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
let entry_count = entries.count();
|
||||
detailed_output.push_str(&format!(
|
||||
"\nCopied {} top-level items to workspace\n",
|
||||
entry_count
|
||||
));
|
||||
}
|
||||
|
||||
detailed_output
|
||||
@@ -1236,13 +1209,15 @@ async fn execute_step(ctx: StepExecutionContext<'_>) -> Result<StepResult, Execu
|
||||
let mut owned_strings: Vec<String> = Vec::new(); // Keep strings alive until after we use cmd
|
||||
|
||||
// Special handling for Rust actions
|
||||
if uses.starts_with("actions-rs/") {
|
||||
if uses.starts_with("actions-rs/") || uses.starts_with("dtolnay/rust-toolchain") {
|
||||
wrkflw_logging::info(
|
||||
"🔄 Detected Rust action - using system Rust installation",
|
||||
);
|
||||
|
||||
// For toolchain action, verify Rust is installed
|
||||
if uses.starts_with("actions-rs/toolchain@") {
|
||||
if uses.starts_with("actions-rs/toolchain@")
|
||||
|| uses.starts_with("dtolnay/rust-toolchain@")
|
||||
{
|
||||
let rustc_version = Command::new("rustc")
|
||||
.arg("--version")
|
||||
.output()
|
||||
@@ -1568,7 +1543,7 @@ async fn execute_step(ctx: StepExecutionContext<'_>) -> Result<StepResult, Execu
|
||||
let output = ctx
|
||||
.runtime
|
||||
.run_container(
|
||||
ctx.runner_image,
|
||||
&image,
|
||||
&cmd.to_vec(),
|
||||
&env_vars,
|
||||
container_workspace,
|
||||
@@ -1799,7 +1774,60 @@ async fn execute_step(ctx: StepExecutionContext<'_>) -> Result<StepResult, Execu
|
||||
Ok(step_result)
|
||||
}
|
||||
|
||||
/// Create a gitignore matcher for the given directory
|
||||
fn create_gitignore_matcher(
|
||||
dir: &Path,
|
||||
) -> Result<Option<ignore::gitignore::Gitignore>, ExecutionError> {
|
||||
let mut builder = GitignoreBuilder::new(dir);
|
||||
|
||||
// Try to add .gitignore file if it exists
|
||||
let gitignore_path = dir.join(".gitignore");
|
||||
if gitignore_path.exists() {
|
||||
builder.add(&gitignore_path);
|
||||
}
|
||||
|
||||
// Add some common ignore patterns as fallback
|
||||
builder.add_line(None, "target/").map_err(|e| {
|
||||
ExecutionError::Execution(format!("Failed to add default ignore pattern: {}", e))
|
||||
})?;
|
||||
builder.add_line(None, ".git/").map_err(|e| {
|
||||
ExecutionError::Execution(format!("Failed to add default ignore pattern: {}", e))
|
||||
})?;
|
||||
|
||||
match builder.build() {
|
||||
Ok(gitignore) => Ok(Some(gitignore)),
|
||||
Err(e) => {
|
||||
wrkflw_logging::warning(&format!("Failed to build gitignore matcher: {}", e));
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_directory_contents(from: &Path, to: &Path) -> Result<(), ExecutionError> {
|
||||
copy_directory_contents_with_gitignore(from, to, None)
|
||||
}
|
||||
|
||||
fn copy_directory_contents_with_gitignore(
|
||||
from: &Path,
|
||||
to: &Path,
|
||||
gitignore: Option<&ignore::gitignore::Gitignore>,
|
||||
) -> Result<(), ExecutionError> {
|
||||
// If no gitignore provided, try to create one for the root directory
|
||||
let root_gitignore;
|
||||
let gitignore = if gitignore.is_none() {
|
||||
root_gitignore = create_gitignore_matcher(from)?;
|
||||
root_gitignore.as_ref()
|
||||
} else {
|
||||
gitignore
|
||||
};
|
||||
|
||||
// Log summary of the copy operation
|
||||
wrkflw_logging::debug(&format!(
|
||||
"Copying directory contents from {} to {}",
|
||||
from.display(),
|
||||
to.display()
|
||||
));
|
||||
|
||||
for entry in std::fs::read_dir(from)
|
||||
.map_err(|e| ExecutionError::Execution(format!("Failed to read directory: {}", e)))?
|
||||
{
|
||||
@@ -1807,7 +1835,23 @@ fn copy_directory_contents(from: &Path, to: &Path) -> Result<(), ExecutionError>
|
||||
entry.map_err(|e| ExecutionError::Execution(format!("Failed to read entry: {}", e)))?;
|
||||
let path = entry.path();
|
||||
|
||||
// Skip hidden files/dirs and target directory for efficiency
|
||||
// Check if the file should be ignored according to .gitignore
|
||||
if let Some(gitignore) = gitignore {
|
||||
let relative_path = path.strip_prefix(from).unwrap_or(&path);
|
||||
match gitignore.matched(relative_path, path.is_dir()) {
|
||||
Match::Ignore(_) => {
|
||||
wrkflw_logging::debug(&format!("Skipping ignored file/directory: {path:?}"));
|
||||
continue;
|
||||
}
|
||||
Match::Whitelist(_) | Match::None => {
|
||||
// File is not ignored or explicitly whitelisted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log individual files only in trace mode (removed verbose per-file logging)
|
||||
|
||||
// Additional basic filtering for hidden files (but allow .gitignore and .github)
|
||||
let file_name = match path.file_name() {
|
||||
Some(name) => name.to_string_lossy(),
|
||||
None => {
|
||||
@@ -1817,7 +1861,13 @@ fn copy_directory_contents(from: &Path, to: &Path) -> Result<(), ExecutionError>
|
||||
)));
|
||||
}
|
||||
};
|
||||
if file_name.starts_with(".") || file_name == "target" {
|
||||
|
||||
// Skip most hidden files but allow important ones
|
||||
if file_name.starts_with(".")
|
||||
&& file_name != ".gitignore"
|
||||
&& file_name != ".github"
|
||||
&& !file_name.starts_with(".env")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1835,8 +1885,8 @@ fn copy_directory_contents(from: &Path, to: &Path) -> Result<(), ExecutionError>
|
||||
std::fs::create_dir_all(&dest_path)
|
||||
.map_err(|e| ExecutionError::Execution(format!("Failed to create dir: {}", e)))?;
|
||||
|
||||
// Recursively copy subdirectories
|
||||
copy_directory_contents(&path, &dest_path)?;
|
||||
// Recursively copy subdirectories with the same gitignore
|
||||
copy_directory_contents_with_gitignore(&path, &dest_path, gitignore)?;
|
||||
} else {
|
||||
std::fs::copy(&path, &dest_path)
|
||||
.map_err(|e| ExecutionError::Execution(format!("Failed to copy file: {}", e)))?;
|
||||
@@ -1849,11 +1899,11 @@ fn copy_directory_contents(from: &Path, to: &Path) -> Result<(), ExecutionError>
|
||||
fn get_runner_image(runs_on: &str) -> String {
|
||||
// Map GitHub runners to Docker images
|
||||
match runs_on.trim() {
|
||||
// ubuntu runners - micro images (minimal size)
|
||||
"ubuntu-latest" => "node:16-buster-slim",
|
||||
"ubuntu-22.04" => "node:16-bullseye-slim",
|
||||
"ubuntu-20.04" => "node:16-buster-slim",
|
||||
"ubuntu-18.04" => "node:16-buster-slim",
|
||||
// ubuntu runners - using Ubuntu base images for better compatibility
|
||||
"ubuntu-latest" => "ubuntu:latest",
|
||||
"ubuntu-22.04" => "ubuntu:22.04",
|
||||
"ubuntu-20.04" => "ubuntu:20.04",
|
||||
"ubuntu-18.04" => "ubuntu:18.04",
|
||||
|
||||
// ubuntu runners - medium images (with more tools)
|
||||
"ubuntu-latest-medium" => "catthehacker/ubuntu:act-latest",
|
||||
|
||||
@@ -23,6 +23,7 @@ serde_yaml.workspace = true
|
||||
tempfile = "3.9"
|
||||
tokio.workspace = true
|
||||
futures = "0.3"
|
||||
ignore = "0.4"
|
||||
wrkflw-utils = { path = "../utils", version = "0.7.0" }
|
||||
which = "4.4"
|
||||
regex = "1.10"
|
||||
|
||||
@@ -10,6 +10,8 @@ use tempfile::TempDir;
|
||||
use which;
|
||||
use wrkflw_logging;
|
||||
|
||||
use ignore::{gitignore::GitignoreBuilder, Match};
|
||||
|
||||
// Global collection of resources to clean up
|
||||
static EMULATION_WORKSPACES: Lazy<Mutex<Vec<PathBuf>>> = Lazy::new(|| Mutex::new(Vec::new()));
|
||||
static EMULATION_PROCESSES: Lazy<Mutex<Vec<u32>>> = Lazy::new(|| Mutex::new(Vec::new()));
|
||||
@@ -490,14 +492,75 @@ impl ContainerRuntime for EmulationRuntime {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Create a gitignore matcher for the given directory
|
||||
fn create_gitignore_matcher(
|
||||
dir: &Path,
|
||||
) -> Result<Option<ignore::gitignore::Gitignore>, std::io::Error> {
|
||||
let mut builder = GitignoreBuilder::new(dir);
|
||||
|
||||
// Try to add .gitignore file if it exists
|
||||
let gitignore_path = dir.join(".gitignore");
|
||||
if gitignore_path.exists() {
|
||||
builder.add(&gitignore_path);
|
||||
}
|
||||
|
||||
// Add some common ignore patterns as fallback
|
||||
if let Err(e) = builder.add_line(None, "target/") {
|
||||
wrkflw_logging::warning(&format!("Failed to add default ignore pattern: {}", e));
|
||||
}
|
||||
if let Err(e) = builder.add_line(None, ".git/") {
|
||||
wrkflw_logging::warning(&format!("Failed to add default ignore pattern: {}", e));
|
||||
}
|
||||
|
||||
match builder.build() {
|
||||
Ok(gitignore) => Ok(Some(gitignore)),
|
||||
Err(e) => {
|
||||
wrkflw_logging::warning(&format!("Failed to build gitignore matcher: {}", e));
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_directory_contents(source: &Path, dest: &Path) -> std::io::Result<()> {
|
||||
copy_directory_contents_with_gitignore(source, dest, None)
|
||||
}
|
||||
|
||||
fn copy_directory_contents_with_gitignore(
|
||||
source: &Path,
|
||||
dest: &Path,
|
||||
gitignore: Option<&ignore::gitignore::Gitignore>,
|
||||
) -> std::io::Result<()> {
|
||||
// Create the destination directory if it doesn't exist
|
||||
fs::create_dir_all(dest)?;
|
||||
|
||||
// If no gitignore provided, try to create one for the root directory
|
||||
let root_gitignore;
|
||||
let gitignore = if gitignore.is_none() {
|
||||
root_gitignore = create_gitignore_matcher(source)?;
|
||||
root_gitignore.as_ref()
|
||||
} else {
|
||||
gitignore
|
||||
};
|
||||
|
||||
// Iterate through all entries in the source directory
|
||||
for entry in fs::read_dir(source)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
// Check if the file should be ignored according to .gitignore
|
||||
if let Some(gitignore) = gitignore {
|
||||
let relative_path = path.strip_prefix(source).unwrap_or(&path);
|
||||
match gitignore.matched(relative_path, path.is_dir()) {
|
||||
Match::Ignore(_) => {
|
||||
wrkflw_logging::debug(&format!("Skipping ignored file/directory: {path:?}"));
|
||||
continue;
|
||||
}
|
||||
Match::Whitelist(_) | Match::None => {
|
||||
// File is not ignored or explicitly whitelisted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let file_name = match path.file_name() {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
@@ -507,23 +570,19 @@ fn copy_directory_contents(source: &Path, dest: &Path) -> std::io::Result<()> {
|
||||
};
|
||||
let dest_path = dest.join(file_name);
|
||||
|
||||
// Skip hidden files (except .gitignore and .github might be useful)
|
||||
// Skip most hidden files but allow important ones
|
||||
let file_name_str = file_name.to_string_lossy();
|
||||
if file_name_str.starts_with(".")
|
||||
&& file_name_str != ".gitignore"
|
||||
&& file_name_str != ".github"
|
||||
&& !file_name_str.starts_with(".env")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip target directory for Rust projects
|
||||
if file_name_str == "target" {
|
||||
continue;
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
// Recursively copy subdirectories
|
||||
copy_directory_contents(&path, &dest_path)?;
|
||||
// Recursively copy subdirectories with the same gitignore
|
||||
copy_directory_contents_with_gitignore(&path, &dest_path, gitignore)?;
|
||||
} else {
|
||||
// Copy files
|
||||
fs::copy(&path, &dest_path)?;
|
||||
@@ -584,6 +643,15 @@ pub async fn handle_special_action(action: &str) -> Result<(), ContainerError> {
|
||||
wrkflw_logging::info(&format!("🔄 Detected Rust formatter action: {}", action));
|
||||
|
||||
check_command_available("rustfmt", "rustfmt", "rustup component add rustfmt");
|
||||
} else if action.starts_with("dtolnay/rust-toolchain@") {
|
||||
// For dtolnay/rust-toolchain action, check for Rust installation
|
||||
wrkflw_logging::info(&format!(
|
||||
"🔄 Detected dtolnay Rust toolchain action: {}",
|
||||
action
|
||||
));
|
||||
|
||||
check_command_available("rustc", "Rust", "https://rustup.rs/");
|
||||
check_command_available("cargo", "Cargo", "https://rustup.rs/");
|
||||
} else if action.starts_with("actions/setup-node@") {
|
||||
// Node.js setup action
|
||||
wrkflw_logging::info(&format!("🔄 Detected Node.js setup action: {}", action));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "wrkflw-secrets"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
edition = "2021"
|
||||
authors = ["wrkflw contributors"]
|
||||
description = "Secrets management for wrkflw workflow execution"
|
||||
|
||||
Reference in New Issue
Block a user