mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-22 08:02:40 +00:00
* feat: add powershell common parameters support (-Verbose, -Debug, -ErrorAction, -WhatIf) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add powershell common params to script editor test panel Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: detect CmdletBinding from code instead of schema in script editor Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: ignore commented-out CmdletBinding in powershell detection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use preference variables for -Verbose/-Debug instead of CLI args Verbose/Debug output goes to PowerShell stream 4/5 which isn't captured by the 2>&1 redirect. Setting $VerbosePreference/$DebugPreference in the wrapper scope propagates to child scripts and output flows through the host to stderr, which Windmill captures as logs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use *>&1 to capture all powershell streams including verbose/debug The previous 2>&1 only captured error stream. Verbose (stream 4) and debug (stream 5) output was silently lost. Using *>&1 redirects all streams to success stream so they flow through Tee-Object into logs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use targeted stream redirects (4>&1 5>&1 2>&1) instead of *>&1 *>&1 breaks $PSCmdlet.ShouldProcess() by redirecting internal streams. Only redirect verbose (4), debug (5), and error (2) to success stream. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: revert to 2>&1 redirect — stream 4/5 redirects break powershell Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use 4>&1 5>&1 for verbose/debug capture, remove WhatIf support Stream 4/5 redirects capture verbose/debug in the pipeline. WhatIf is removed because $PSCmdlet.ShouldProcess() doesn't work when scripts are invoked through Windmill's wrapper. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: redirect verbose/debug to files to keep result pipeline clean Verbose (4) and debug (5) streams are redirected to separate log files during script execution, then output via Write-Host after the script completes. This keeps them out of the Tee-Object pipeline (used for result extraction) while still showing them in the job logs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: output verbose/debug to stderr via Console.Error for log capture Write-Host goes to stdout which gets mixed with result output and truncated by OSS log threshold. Using [Console]::Error.WriteLine() writes to stderr which Windmill captures separately as logs, with VERBOSE:/DEBUG: prefixes for clarity. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: redirect script output to file only, send verbose/debug to stdout The OSS log storage has a 9KB threshold. Previously, Tee-Object sent the full JSON result to both stdout (logs) and the pipe file, eating the log budget. Now script output goes only to the pipe file (> $pipe), and only verbose/debug messages go to stdout for the log viewer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: preserve original Tee-Object behavior, append verbose/debug after Keep the original wrapper behavior (Tee-Object to stdout + pipe file). Only add 4>verbose.log 5>debug.log to capture those streams, and output them at the end of logs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: inject preference vars into main.ps1 instead of CLI args Passing -Verbose/-Debug as CLI args causes PowerShell module loading to emit verbose noise. Instead, inject $VerbosePreference/$DebugPreference inside main.ps1's try block so they only affect user code. Stream 4/5 are still redirected to files in the wrapper for log output. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: restore common param toggles from previous job args on Run Again Extract _wm_ps_* keys from loaded args and initialize the toggle states in PowerShellCommonParams. Also strip them from main args so they don't appear as unknown schema form inputs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: show active common param badges when section is collapsed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: inject ErrorAction as preference variable instead of CLI arg -ErrorAction as a CLI arg only affects the caller, not the script's internal error handling. Setting $ErrorActionPreference inside main.ps1 correctly overrides the default 'Stop' behavior for the user's code. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: ensure full backward compatibility with existing powershell scripts - Only filter common param names when [CmdletBinding()] is present (without it, $Verbose etc. are regular user-defined parameters) - Only add 4>verbose.log 5>debug.log and log output lines when common params are actually enabled — original wrapper is unchanged otherwise Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: lighter styling for common params section Replaced heavy Section component with a subtle inline chevron toggle labeled "Common parameters". Smaller text, secondary color, indented options. Badges still show when collapsed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: rename section to CmdletBinding parameters Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add ..Default::default() to windmill-parser-r (new parser from main) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: missing comma in graphql parser test + merge main Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add missing commas before ..Default::default() in parser tests Merge from main brought test constructors with formatting issues from the original automated script (missing comma between last field and ..Default::default()). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: restore comment markers in nu parser test that script broke Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review — ignore commented CmdletBinding, clear stale params 1. Parser: strip comment lines before detecting [CmdletBinding()] to avoid false positives from commented-out attributes 2. RunForm: always assign psCommonParams (not just when non-empty) so stale settings from a previous run don't leak into later runs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
416 lines
15 KiB
Rust
416 lines
15 KiB
Rust
#![cfg_attr(target_arch = "wasm32", feature(c_variadic))]
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
pub mod wasm_libc;
|
|
|
|
use anyhow::anyhow;
|
|
use anyhow::bail;
|
|
use regex::Regex;
|
|
use serde_json::Value;
|
|
use tree_sitter::Node;
|
|
use tree_sitter::Range;
|
|
use windmill_parser::json_to_typ;
|
|
use windmill_parser::Arg;
|
|
use windmill_parser::MainArgSignature;
|
|
|
|
pub fn parse_ruby_sig_meta(code: &str) -> anyhow::Result<MainArgSignature> {
|
|
let mut parser = tree_sitter::Parser::new();
|
|
let language = tree_sitter_ruby::LANGUAGE;
|
|
parser
|
|
.set_language(&language.into())
|
|
.map_err(|e| anyhow!("Error setting Ruby as language: {e}"))?;
|
|
|
|
// Parse code
|
|
let tree = parser
|
|
.parse(code, None)
|
|
.ok_or(anyhow!("Failed to parse code"))?;
|
|
let root_node = tree.root_node();
|
|
|
|
root_node.clone().to_string();
|
|
// Traverse the AST to find the Main method signature
|
|
let args = find_main_signature(root_node, code)?;
|
|
let auto_kind = if args.is_none() {
|
|
Some("lib".to_string())
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let main_sig = MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: args.unwrap_or_default(),
|
|
has_preprocessor: None,
|
|
auto_kind,
|
|
..Default::default()
|
|
};
|
|
|
|
Ok(main_sig)
|
|
}
|
|
|
|
pub fn parse_ruby_signature(code: &str) -> anyhow::Result<MainArgSignature> {
|
|
Ok(parse_ruby_sig_meta(code)?)
|
|
}
|
|
|
|
pub fn parse_ruby_requirements(code: &str) -> anyhow::Result<String> {
|
|
let mut parser = tree_sitter::Parser::new();
|
|
let language = tree_sitter_ruby::LANGUAGE;
|
|
parser
|
|
.set_language(&language.into())
|
|
.map_err(|e| anyhow!("Error setting Ruby as language: {e}"))?;
|
|
|
|
// Parse code
|
|
let tree = parser
|
|
.parse(code, None)
|
|
.ok_or(anyhow!("Failed to parse code"))?;
|
|
let root_node = tree.root_node();
|
|
|
|
root_node.clone().to_string();
|
|
let mut cursor = root_node.walk();
|
|
'top_level: for x in root_node.children(&mut cursor) {
|
|
if x.kind() == "call" {
|
|
for (i, n) in x.children(&mut x.walk()).enumerate() {
|
|
if i == 0
|
|
&& n.kind() == "identifier"
|
|
&& !n
|
|
.utf8_text(code.as_bytes())
|
|
.map(|ident| ident == "gemfile")
|
|
.unwrap_or_default()
|
|
{
|
|
continue 'top_level;
|
|
} else if n.kind() == "do_block" {
|
|
let req = n.utf8_text(code.as_bytes())?.to_owned();
|
|
|
|
lazy_static::lazy_static! {
|
|
static ref WINDMILL_RE: Regex = Regex::new(r"(?m)^\s*require\s*'windmill/inline'").unwrap();
|
|
}
|
|
|
|
if WINDMILL_RE.find(&code).is_none() {
|
|
return Err(anyhow!(
|
|
"`require 'windmill/inline'` is not detected - please add `require 'windmill/inline'` in order to use inline gemfile.
|
|
Your Gemfile syntax will be compatible with bundler/inline."
|
|
)
|
|
.into());
|
|
}
|
|
|
|
return req
|
|
// gemfile do_block comes with 'do' and 'end'
|
|
// we want to omit these by taking slice
|
|
.get(2..(req.len() - 3))
|
|
.map(str::to_owned)
|
|
.ok_or(anyhow!("Invalid gemfile do block"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(String::new())
|
|
}
|
|
|
|
// Function to find the Main method's signature
|
|
fn find_main_signature<'a>(root_node: Node<'a>, code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
|
|
let mut cursor = root_node.walk();
|
|
'top_level: for x in root_node.children(&mut cursor) {
|
|
if x.kind() == "method" {
|
|
let mut args = vec![];
|
|
for (i, m) in x.children(&mut x.walk()).skip(1).enumerate() {
|
|
if i == 0
|
|
&& m.kind() == "identifier"
|
|
&& !m
|
|
.utf8_text(code.as_bytes())
|
|
.map(|ident| ident == "main")
|
|
.unwrap_or_default()
|
|
{
|
|
continue 'top_level;
|
|
} else if m.kind() == "method_parameters" {
|
|
for a in m.children(&mut m.walk()) {
|
|
if a.kind() == "identifier" {
|
|
a.utf8_text(code.as_bytes()).inspect(|name| {
|
|
args.push(Arg { name: (*name).to_owned(), ..Default::default() })
|
|
})?;
|
|
}
|
|
if a.kind() == "optional_parameter" {
|
|
let mut walk = a.walk();
|
|
let mut it = a.children(&mut walk).into_iter();
|
|
match (it.next().and_then(|n| n.utf8_text(code.as_bytes()).ok()), {
|
|
// Skip `=`
|
|
it.next();
|
|
it.next().map(|n| n.range())
|
|
}) {
|
|
(Some(ident), Some(Range { start_byte, end_byte, .. })) => {
|
|
let unparsed =
|
|
&code[start_byte..end_byte].replace("nil", "null");
|
|
let default: Value = serde_json::from_str(unparsed).map_err(
|
|
|e|
|
|
anyhow!("Cannot convert default value to json\n\tvalue: {unparsed}\n\terror: {e}{}",
|
|
if e.to_string().contains("key must be a string") {
|
|
"\n\nNOTE: If you are trying to declare default hash, use following syntax:\n { \"<key>\": <value> }"
|
|
} else {
|
|
""
|
|
}
|
|
))?;
|
|
args.push(Arg {
|
|
name: ident.to_owned(),
|
|
typ: json_to_typ(&default, true),
|
|
default: Some(default),
|
|
has_default: true,
|
|
..Default::default()
|
|
});
|
|
}
|
|
_ => {
|
|
let Range { start_byte, end_byte, .. } = a.range();
|
|
bail!(
|
|
"Cannot parse optional parameter: {}",
|
|
&code.get(start_byte..end_byte).unwrap_or("CANNOT DISPLAY")
|
|
)
|
|
}
|
|
}
|
|
}
|
|
let kind = a.kind();
|
|
if matches!(
|
|
kind,
|
|
"keyword_parameter" | "splat_parameter" | "hash_splat_parameter"
|
|
) {
|
|
let Range { start_byte, end_byte, .. } = a.range();
|
|
bail!(
|
|
" - {}\n{}s are not supported",
|
|
&code.get(start_byte..end_byte).unwrap_or("CANNOT DISPLAY"),
|
|
kind.replace("_", " ")
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return Ok(Some(args));
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
|
|
use serde_json::json;
|
|
use windmill_parser::{ObjectProperty, ObjectType, Typ};
|
|
|
|
use super::parse_ruby_sig_meta as parse;
|
|
use super::*;
|
|
#[test]
|
|
fn test_parse_ruby_no_main() {
|
|
let code = r#"
|
|
def not_main end
|
|
def private_fn end
|
|
"#;
|
|
let sig = parse(code).unwrap();
|
|
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature { auto_kind: Some("lib".to_string()), ..Default::default() }
|
|
);
|
|
}
|
|
#[test]
|
|
fn test_parse_ruby_no_args() {
|
|
let code = r#"
|
|
def main
|
|
end
|
|
"#;
|
|
let sig = parse(code).unwrap();
|
|
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature { auto_kind: None, ..Default::default() }
|
|
);
|
|
}
|
|
#[test]
|
|
fn test_parse_ruby_positional_args() {
|
|
let sig = {
|
|
let code = r#"def main(a, b, c) end"#;
|
|
parse(code).unwrap()
|
|
};
|
|
let sig2 = {
|
|
let code = r#"
|
|
def main a, b, c
|
|
end"#;
|
|
parse(code).unwrap()
|
|
};
|
|
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
args: vec![
|
|
Arg { name: "a".into(), ..Default::default() },
|
|
Arg { name: "b".into(), ..Default::default() },
|
|
Arg { name: "c".into(), ..Default::default() }
|
|
],
|
|
auto_kind: None,
|
|
..Default::default()
|
|
}
|
|
);
|
|
assert_eq!(sig2, sig);
|
|
}
|
|
#[test]
|
|
fn test_parse_ruby_lists() {
|
|
let sig = {
|
|
let code = r#"def main(a = [ 1, 2, 3 ], b = [ 1, false, nil, "test"]) end"#;
|
|
parse(code).unwrap()
|
|
};
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
args: vec![
|
|
Arg {
|
|
name: "a".into(),
|
|
has_default: true,
|
|
typ: Typ::List(Box::new(Typ::Int)),
|
|
default: Some(json!([1, 2, 3])),
|
|
..Default::default()
|
|
},
|
|
Arg {
|
|
name: "b".into(),
|
|
has_default: true,
|
|
typ: Typ::List(Box::new(Typ::Unknown)),
|
|
default: Some(json!([1, false, null, "test"])),
|
|
..Default::default()
|
|
},
|
|
],
|
|
auto_kind: None,
|
|
..Default::default()
|
|
}
|
|
);
|
|
}
|
|
#[test]
|
|
fn test_parse_ruby_hashes() {
|
|
let sig = {
|
|
let code = r#"def main(a = { "1": 4, "2": [ 1, 2, 3] }) end"#;
|
|
parse(code).unwrap()
|
|
};
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
args: vec![Arg {
|
|
name: "a".into(),
|
|
has_default: true,
|
|
typ: Typ::Object(ObjectType::new(
|
|
None,
|
|
Some(vec![
|
|
ObjectProperty { key: "1".into(), typ: Box::new(Typ::Int) },
|
|
ObjectProperty {
|
|
key: "2".into(),
|
|
typ: Box::new(Typ::List(Box::new(Typ::Int)))
|
|
}
|
|
],)
|
|
)),
|
|
default: Some(json!({"1": 4, "2": [ 1, 2, 3 ]})),
|
|
..Default::default()
|
|
},],
|
|
auto_kind: None,
|
|
..Default::default()
|
|
}
|
|
);
|
|
}
|
|
#[test]
|
|
fn test_parse_ruby_default_args() {
|
|
let sig = {
|
|
let code =
|
|
r#"def main(a = 10, b = "hey", c = false, d = [ 1, 2, 3 ], e = { "a": 43 }) end"#;
|
|
parse(code).unwrap()
|
|
};
|
|
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
args: vec![
|
|
Arg {
|
|
name: "a".into(),
|
|
has_default: true,
|
|
typ: Typ::Int,
|
|
default: Some(json!(10)),
|
|
..Default::default()
|
|
},
|
|
Arg {
|
|
name: "b".into(),
|
|
has_default: true,
|
|
typ: Typ::Str(None),
|
|
default: Some(json!("hey")),
|
|
..Default::default()
|
|
},
|
|
Arg {
|
|
name: "c".into(),
|
|
has_default: true,
|
|
typ: Typ::Bool,
|
|
default: Some(json!(false)),
|
|
..Default::default()
|
|
},
|
|
Arg {
|
|
name: "d".into(),
|
|
has_default: true,
|
|
typ: Typ::List(Box::new(Typ::Int)),
|
|
default: Some(json!([1, 2, 3])),
|
|
..Default::default()
|
|
},
|
|
Arg {
|
|
name: "e".into(),
|
|
has_default: true,
|
|
typ: Typ::Object(ObjectType::new(
|
|
None,
|
|
Some(vec![ObjectProperty {
|
|
key: "a".into(),
|
|
typ: Box::new(Typ::Int)
|
|
}])
|
|
)),
|
|
default: Some(json!({"a": 43})),
|
|
..Default::default()
|
|
},
|
|
],
|
|
auto_kind: None,
|
|
..Default::default()
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_ruby_unsupported() {
|
|
assert_eq!(
|
|
&parse("def main( a: ) end").unwrap_err().to_string(),
|
|
" - a:\nkeyword parameters are not supported"
|
|
);
|
|
|
|
assert_eq!(
|
|
&parse("def main( *a ) end").unwrap_err().to_string(),
|
|
" - *a\nsplat parameters are not supported"
|
|
);
|
|
|
|
assert_eq!(
|
|
&parse("def main( **a ) end").unwrap_err().to_string(),
|
|
" - **a\nhash splat parameters are not supported"
|
|
);
|
|
|
|
assert!(&parse("def main( a = Time.now ) end").is_err());
|
|
assert!(&parse("def main( a = :symbol ) end").is_err());
|
|
assert!(&parse("def main( a = { b: 2 } ) end").is_err());
|
|
assert!(&parse("def main( a = { b => 2 } ) end").is_err());
|
|
assert!(&parse(r#"def main( a = { "b" => 2 } ) end"#).is_err());
|
|
}
|
|
|
|
// #[test]
|
|
// fn test_parse_ruby_requirements() {
|
|
// assert_eq!(
|
|
// parse_ruby_requirements(
|
|
// "
|
|
// require 'dep1'
|
|
// require 'dep2'
|
|
// require 'dep2/submod'
|
|
// require 'u/username/script'
|
|
// require 'f/folder/script'
|
|
// "
|
|
// )
|
|
// .unwrap(),
|
|
// vec![
|
|
// "dep1".to_owned(),
|
|
// "dep2".into(),
|
|
// "dep2".into(),
|
|
// "u/username/script".into(),
|
|
// "f/folder/script".into(),
|
|
// ]
|
|
// );
|
|
// }
|
|
}
|