mirror of
https://github.com/bahdotsh/wrkflw.git
synced 2025-12-30 17:06:56 +01:00
Compare commits
18 Commits
feature/gi
...
wrkflw-run
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ac18f3715 | ||
|
|
1f3fee7373 | ||
|
|
f49ccd70d9 | ||
|
|
5161882989 | ||
|
|
5e9658c885 | ||
|
|
aa9da33b30 | ||
|
|
dff3697052 | ||
|
|
5051f71b8b | ||
|
|
64b980d254 | ||
|
|
2d809388a2 | ||
|
|
03af6cb7c1 | ||
|
|
ae52779e11 | ||
|
|
fe7be3e1ae | ||
|
|
30f405ccb9 | ||
|
|
1d56d86ba5 | ||
|
|
f1ca411281 | ||
|
|
797e31e3d3 | ||
|
|
4e66f65de7 |
16
.github/workflows/build.yml
vendored
16
.github/workflows/build.yml
vendored
@@ -3,7 +3,7 @@ name: Build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
@@ -12,12 +12,14 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -31,27 +33,27 @@ jobs:
|
||||
target: ${{ matrix.target }}
|
||||
override: true
|
||||
components: clippy, rustfmt
|
||||
|
||||
|
||||
- name: Check formatting
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: -- --check
|
||||
|
||||
|
||||
- name: Run clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: -- -D warnings
|
||||
|
||||
|
||||
- name: Build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --target ${{ matrix.target }}
|
||||
|
||||
|
||||
- name: Run tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --target ${{ matrix.target }}
|
||||
args: --target ${{ matrix.target }}
|
||||
|
||||
930
Cargo.lock
generated
930
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.2"
|
||||
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") {
|
||||
|
||||
@@ -570,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1160,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
|
||||
@@ -1224,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()
|
||||
@@ -1556,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,
|
||||
@@ -1834,6 +1821,13 @@ fn copy_directory_contents_with_gitignore(
|
||||
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)))?
|
||||
{
|
||||
@@ -1855,7 +1849,7 @@ fn copy_directory_contents_with_gitignore(
|
||||
}
|
||||
}
|
||||
|
||||
wrkflw_logging::debug(&format!("Copying entry: {path:?} -> {to:?}"));
|
||||
// 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() {
|
||||
@@ -1905,11 +1899,11 @@ fn copy_directory_contents_with_gitignore(
|
||||
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",
|
||||
|
||||
@@ -643,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));
|
||||
@@ -784,7 +793,7 @@ async fn cleanup_processes() {
|
||||
let _ = Command::new("taskkill")
|
||||
.arg("/F")
|
||||
.arg("/PID")
|
||||
.arg(&pid.to_string())
|
||||
.arg(pid.to_string())
|
||||
.output();
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -17,4 +17,6 @@ wrkflw-models = { path = "../models", version = "0.7.0" }
|
||||
# External dependencies
|
||||
serde.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix.workspace = true
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Shared helpers used across crates.
|
||||
|
||||
- Workflow file detection (`.github/workflows/*.yml`, `.gitlab-ci.yml`)
|
||||
- File-descriptor redirection utilities for silencing noisy subprocess output
|
||||
- File-descriptor redirection utilities for silencing noisy subprocess output (Unix only; Windows support is limited)
|
||||
|
||||
### Example
|
||||
|
||||
@@ -14,7 +14,7 @@ use wrkflw_utils::{is_workflow_file, fd::with_stderr_to_null};
|
||||
assert!(is_workflow_file(Path::new(".github/workflows/ci.yml")));
|
||||
|
||||
let value = with_stderr_to_null(|| {
|
||||
eprintln!("this is hidden");
|
||||
eprintln!("this is hidden on Unix, visible on Windows");
|
||||
42
|
||||
}).unwrap();
|
||||
assert_eq!(value, 42);
|
||||
|
||||
@@ -35,78 +35,145 @@ pub fn is_workflow_file(path: &Path) -> bool {
|
||||
}
|
||||
|
||||
/// Module for safely handling file descriptor redirection
|
||||
///
|
||||
/// On Unix systems (Linux, macOS), this module provides true file descriptor
|
||||
/// redirection by duplicating stderr and redirecting it to /dev/null.
|
||||
///
|
||||
/// On Windows systems, the redirection functionality is limited due to platform
|
||||
/// differences in file descriptor handling. The functions will execute without
|
||||
/// error but stderr may not be fully suppressed.
|
||||
pub mod fd {
|
||||
use nix::fcntl::{open, OFlag};
|
||||
use nix::sys::stat::Mode;
|
||||
use nix::unistd::{close, dup, dup2};
|
||||
use std::io::{self, Result};
|
||||
use std::os::unix::io::RawFd;
|
||||
use std::path::Path;
|
||||
|
||||
/// Standard file descriptors
|
||||
const STDERR_FILENO: RawFd = 2;
|
||||
use std::io::Result;
|
||||
|
||||
/// Represents a redirected stderr that can be restored
|
||||
pub struct RedirectedStderr {
|
||||
original_fd: Option<RawFd>,
|
||||
null_fd: Option<RawFd>,
|
||||
#[cfg(unix)]
|
||||
original_fd: Option<std::os::unix::io::RawFd>,
|
||||
#[cfg(unix)]
|
||||
null_fd: Option<std::os::unix::io::RawFd>,
|
||||
#[cfg(windows)]
|
||||
_phantom: std::marker::PhantomData<()>,
|
||||
}
|
||||
|
||||
impl RedirectedStderr {
|
||||
/// Creates a new RedirectedStderr that redirects stderr to /dev/null
|
||||
pub fn to_null() -> Result<Self> {
|
||||
// Duplicate the current stderr fd
|
||||
let stderr_backup = match dup(STDERR_FILENO) {
|
||||
Ok(fd) => fd,
|
||||
Err(e) => return Err(io::Error::other(e)),
|
||||
};
|
||||
#[cfg(unix)]
|
||||
mod unix_impl {
|
||||
use super::*;
|
||||
use nix::fcntl::{open, OFlag};
|
||||
use nix::sys::stat::Mode;
|
||||
use nix::unistd::{close, dup, dup2};
|
||||
use std::io;
|
||||
use std::os::unix::io::RawFd;
|
||||
use std::path::Path;
|
||||
|
||||
// Open /dev/null
|
||||
let null_fd = match open(Path::new("/dev/null"), OFlag::O_WRONLY, Mode::empty()) {
|
||||
Ok(fd) => fd,
|
||||
Err(e) => {
|
||||
/// Standard file descriptors
|
||||
const STDERR_FILENO: RawFd = 2;
|
||||
|
||||
impl RedirectedStderr {
|
||||
/// Creates a new RedirectedStderr that redirects stderr to /dev/null
|
||||
pub fn to_null() -> Result<Self> {
|
||||
// Duplicate the current stderr fd
|
||||
let stderr_backup = match dup(STDERR_FILENO) {
|
||||
Ok(fd) => fd,
|
||||
Err(e) => return Err(io::Error::other(e)),
|
||||
};
|
||||
|
||||
// Open /dev/null
|
||||
let null_fd = match open(Path::new("/dev/null"), OFlag::O_WRONLY, Mode::empty()) {
|
||||
Ok(fd) => fd,
|
||||
Err(e) => {
|
||||
let _ = close(stderr_backup); // Clean up on error
|
||||
return Err(io::Error::other(e));
|
||||
}
|
||||
};
|
||||
|
||||
// Redirect stderr to /dev/null
|
||||
if let Err(e) = dup2(null_fd, STDERR_FILENO) {
|
||||
let _ = close(stderr_backup); // Clean up on error
|
||||
let _ = close(null_fd);
|
||||
return Err(io::Error::other(e));
|
||||
}
|
||||
};
|
||||
|
||||
// Redirect stderr to /dev/null
|
||||
if let Err(e) = dup2(null_fd, STDERR_FILENO) {
|
||||
let _ = close(stderr_backup); // Clean up on error
|
||||
let _ = close(null_fd);
|
||||
return Err(io::Error::other(e));
|
||||
Ok(RedirectedStderr {
|
||||
original_fd: Some(stderr_backup),
|
||||
null_fd: Some(null_fd),
|
||||
})
|
||||
}
|
||||
|
||||
Ok(RedirectedStderr {
|
||||
original_fd: Some(stderr_backup),
|
||||
null_fd: Some(null_fd),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RedirectedStderr {
|
||||
/// Automatically restores stderr when the RedirectedStderr is dropped
|
||||
fn drop(&mut self) {
|
||||
if let Some(orig_fd) = self.original_fd.take() {
|
||||
// Restore the original stderr
|
||||
let _ = dup2(orig_fd, STDERR_FILENO);
|
||||
let _ = close(orig_fd);
|
||||
}
|
||||
impl Drop for RedirectedStderr {
|
||||
/// Automatically restores stderr when the RedirectedStderr is dropped
|
||||
fn drop(&mut self) {
|
||||
if let Some(orig_fd) = self.original_fd.take() {
|
||||
// Restore the original stderr
|
||||
let _ = dup2(orig_fd, STDERR_FILENO);
|
||||
let _ = close(orig_fd);
|
||||
}
|
||||
|
||||
// Close the null fd
|
||||
if let Some(null_fd) = self.null_fd.take() {
|
||||
let _ = close(null_fd);
|
||||
// Close the null fd
|
||||
if let Some(null_fd) = self.null_fd.take() {
|
||||
let _ = close(null_fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a function with stderr redirected to /dev/null, then restore stderr
|
||||
#[cfg(windows)]
|
||||
mod windows_impl {
|
||||
use super::*;
|
||||
|
||||
impl RedirectedStderr {
|
||||
/// Creates a new RedirectedStderr that redirects stderr to NUL on Windows
|
||||
pub fn to_null() -> Result<Self> {
|
||||
// On Windows, we can't easily redirect stderr at the file descriptor level
|
||||
// like we can on Unix systems. This is a simplified implementation that
|
||||
// doesn't actually redirect but provides the same interface.
|
||||
// The actual stderr suppression will need to be handled differently on Windows.
|
||||
Ok(RedirectedStderr {
|
||||
_phantom: std::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RedirectedStderr {
|
||||
/// No-op drop implementation for Windows
|
||||
fn drop(&mut self) {
|
||||
// Nothing to restore on Windows in this simplified implementation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a function with stderr redirected to /dev/null (Unix) or suppressed (Windows), then restore stderr
|
||||
///
|
||||
/// # Platform Support
|
||||
/// - **Unix (Linux, macOS)**: Fully supported - stderr is redirected to /dev/null
|
||||
/// - **Windows**: Limited support - function executes but stderr may be visible
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use wrkflw_utils::fd::with_stderr_to_null;
|
||||
///
|
||||
/// let result = with_stderr_to_null(|| {
|
||||
/// eprintln!("This will be hidden on Unix");
|
||||
/// 42
|
||||
/// }).unwrap();
|
||||
/// assert_eq!(result, 42);
|
||||
/// ```
|
||||
pub fn with_stderr_to_null<F, T>(f: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce() -> T,
|
||||
{
|
||||
let _redirected = RedirectedStderr::to_null()?;
|
||||
Ok(f())
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let _redirected = RedirectedStderr::to_null()?;
|
||||
Ok(f())
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// On Windows, we can't easily redirect stderr at the FD level,
|
||||
// so we just run the function without redirection.
|
||||
// This means stderr won't be suppressed on Windows, but the function will work.
|
||||
Ok(f())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,15 +183,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_fd_redirection() {
|
||||
// This test will write to stderr, which should be redirected
|
||||
// This test will write to stderr, which should be redirected on Unix
|
||||
// On Windows, it will just run normally without redirection
|
||||
let result = fd::with_stderr_to_null(|| {
|
||||
// This would normally appear in stderr
|
||||
eprintln!("This should be redirected to /dev/null");
|
||||
// This would normally appear in stderr (suppressed on Unix, visible on Windows)
|
||||
eprintln!("This should be redirected to /dev/null on Unix");
|
||||
// Return a test value to verify the function passes through the result
|
||||
42
|
||||
});
|
||||
|
||||
// The function should succeed and return our test value
|
||||
// The function should succeed and return our test value on both platforms
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), 42);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user