diff --git a/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json b/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json index 713ccb9dd3..36ddb8ab9f 100644 --- a/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json +++ b/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json @@ -15,7 +15,7 @@ ] }, "nullable": [ - null + true ] }, "hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55" diff --git a/backend/.sqlx/query-726e956cfcd3ac7c07abeecdf92cf0996efe7fa7b671ac2b3b000ead0ea307de.json b/backend/.sqlx/query-9a1483a81f5b086e0765d3d69483e29b09f66090e1f9d394564c16d921d2e66c.json similarity index 52% rename from backend/.sqlx/query-726e956cfcd3ac7c07abeecdf92cf0996efe7fa7b671ac2b3b000ead0ea307de.json rename to backend/.sqlx/query-9a1483a81f5b086e0765d3d69483e29b09f66090e1f9d394564c16d921d2e66c.json index 4baf932023..e751b8fc7c 100644 --- a/backend/.sqlx/query-726e956cfcd3ac7c07abeecdf92cf0996efe7fa7b671ac2b3b000ead0ea307de.json +++ b/backend/.sqlx/query-9a1483a81f5b086e0765d3d69483e29b09f66090e1f9d394564c16d921d2e66c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT s.hash as hash, dm.deployment_msg as deployment_msg \n FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash\n WHERE s.workspace_id = $1 AND s.path = $2\n ORDER by s.created_at DESC", + "query": "SELECT s.hash as hash, dm.deployment_msg as deployment_msg, s.created_at as created_at\n FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash\n WHERE s.workspace_id = $1 AND s.path = $2\n ORDER by s.created_at DESC", "describe": { "columns": [ { @@ -12,6 +12,11 @@ "ordinal": 1, "name": "deployment_msg", "type_info": "Text" + }, + { + "ordinal": 2, + "name": "created_at", + "type_info": "Timestamptz" } ], "parameters": { @@ -22,8 +27,9 @@ }, "nullable": [ false, - true + true, + false ] }, - "hash": "726e956cfcd3ac7c07abeecdf92cf0996efe7fa7b671ac2b3b000ead0ea307de" + "hash": "9a1483a81f5b086e0765d3d69483e29b09f66090e1f9d394564c16d921d2e66c" } diff --git a/backend/.sqlx/query-cf2a6ad6471a40b6298775cda9300aeecdd75503bed59d80cd62091d1642d1ec.json b/backend/.sqlx/query-c73e98e5a937f44724a96ee1b74d31fa71a7be3b8ba3dec9f59f54a6c4030462.json similarity index 51% rename from backend/.sqlx/query-cf2a6ad6471a40b6298775cda9300aeecdd75503bed59d80cd62091d1642d1ec.json rename to backend/.sqlx/query-c73e98e5a937f44724a96ee1b74d31fa71a7be3b8ba3dec9f59f54a6c4030462.json index 4e17af3ce2..06b2c50058 100644 --- a/backend/.sqlx/query-cf2a6ad6471a40b6298775cda9300aeecdd75503bed59d80cd62091d1642d1ec.json +++ b/backend/.sqlx/query-c73e98e5a937f44724a96ee1b74d31fa71a7be3b8ba3dec9f59f54a6c4030462.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT s.hash as hash, dm.deployment_msg as deployment_msg \n FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash\n WHERE s.workspace_id = $1 AND s.path = $2\n ORDER by s.created_at DESC LIMIT 1", + "query": "SELECT s.hash as hash, dm.deployment_msg as deployment_msg, s.created_at as created_at\n FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash\n WHERE s.workspace_id = $1 AND s.path = $2\n ORDER by s.created_at DESC LIMIT 1", "describe": { "columns": [ { @@ -12,6 +12,11 @@ "ordinal": 1, "name": "deployment_msg", "type_info": "Text" + }, + { + "ordinal": 2, + "name": "created_at", + "type_info": "Timestamptz" } ], "parameters": { @@ -22,8 +27,9 @@ }, "nullable": [ false, - true + true, + false ] }, - "hash": "cf2a6ad6471a40b6298775cda9300aeecdd75503bed59d80cd62091d1642d1ec" + "hash": "c73e98e5a937f44724a96ee1b74d31fa71a7be3b8ba3dec9f59f54a6c4030462" } diff --git a/backend/windmill-api-scripts/src/scripts.rs b/backend/windmill-api-scripts/src/scripts.rs index 5e3c3eb6df..a909fc0ffc 100644 --- a/backend/windmill-api-scripts/src/scripts.rs +++ b/backend/windmill-api-scripts/src/scripts.rs @@ -1447,7 +1447,7 @@ async fn get_script_history( check_scopes(&authed, || format!("scripts:read:{}", path))?; let mut tx = user_db.begin(&authed).await?; let query_result = sqlx::query!( - "SELECT s.hash as hash, dm.deployment_msg as deployment_msg + "SELECT s.hash as hash, dm.deployment_msg as deployment_msg, s.created_at as created_at FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash WHERE s.workspace_id = $1 AND s.path = $2 ORDER by s.created_at DESC", @@ -1463,6 +1463,7 @@ async fn get_script_history( .map(|row| ScriptHistory { script_hash: ScriptHash(row.hash), deployment_msg: row.deployment_msg, + created_at: Some(row.created_at), }) .collect(); return Ok(Json(result)); @@ -1477,7 +1478,7 @@ async fn get_latest_version( check_scopes(&authed, || format!("scripts:read:{}", path))?; let mut tx = user_db.begin(&authed).await?; let row_o = sqlx::query!( - "SELECT s.hash as hash, dm.deployment_msg as deployment_msg + "SELECT s.hash as hash, dm.deployment_msg as deployment_msg, s.created_at as created_at FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash WHERE s.workspace_id = $1 AND s.path = $2 ORDER by s.created_at DESC LIMIT 1", @@ -1491,7 +1492,8 @@ async fn get_latest_version( if let Some(row) = row_o { let result = ScriptHistory { script_hash: ScriptHash(row.hash), - deployment_msg: row.deployment_msg, // + deployment_msg: row.deployment_msg, + created_at: Some(row.created_at), }; return Ok(Json(Some(result))); } else { diff --git a/backend/windmill-types/src/scripts.rs b/backend/windmill-types/src/scripts.rs index de26f0e484..e5a980eaba 100644 --- a/backend/windmill-types/src/scripts.rs +++ b/backend/windmill-types/src/scripts.rs @@ -450,6 +450,8 @@ pub struct ScriptHistory { pub script_hash: ScriptHash, #[serde(skip_serializing_if = "Option::is_none")] pub deployment_msg: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub created_at: Option>, } #[derive(Deserialize)] diff --git a/cli/src/commands/app/lint.ts b/cli/src/commands/app/lint.ts index 12014cc7d5..9356b5dd3c 100644 --- a/cli/src/commands/app/lint.ts +++ b/cli/src/commands/app/lint.ts @@ -6,7 +6,7 @@ import { colors } from "@cliffy/ansi/colors"; import * as log from "../../core/log.ts"; import { yamlParseFile } from "../../utils/yaml.ts"; import { GlobalOptions } from "../../types.ts"; -import { createBundle } from "./bundle.ts"; +import { createBundle, detectFrameworks } from "./bundle.ts"; import { APP_BACKEND_FOLDER } from "./app_metadata.ts"; import { loadRunnablesFromBackend } from "./raw_apps.ts"; import { @@ -113,7 +113,11 @@ async function validateBuild( log.info(colors.blue("🔨 Testing build...")); // Try to create a bundle - this will validate that all dependencies are in place + const frameworks = detectFrameworks(appDir); + const entryFile = frameworks.svelte || frameworks.vue ? "index.ts" : "index.tsx"; + const entryPoint = path.join(appDir, entryFile); await createBundle({ + entryPoint, production: true, minify: false, }); diff --git a/cli/src/commands/flow/flow.ts b/cli/src/commands/flow/flow.ts index ad1a5e4d3f..c66d637e16 100644 --- a/cli/src/commands/flow/flow.ts +++ b/cli/src/commands/flow/flow.ts @@ -204,14 +204,14 @@ export async function pushFlow( type Options = GlobalOptions; -async function push(opts: Options, filePath: string, remotePath: string) { +async function push(opts: Options & { message?: string }, filePath: string, remotePath: string) { if (!validatePath(remotePath)) { return; } const workspace = await resolveWorkspace(opts); await requireLogin(opts); - await pushFlow(workspace.workspaceId, remotePath, filePath); + await pushFlow(workspace.workspaceId, remotePath, filePath, opts.message); log.info(colors.bold.underline.green("Flow pushed")); } @@ -295,6 +295,24 @@ async function run( const input = opts.data ? await resolve(opts.data) : {}; + // Validate required args against schema when no data provided + if (!opts.data) { + try { + const flow = await wmill.getFlowByPath({ + workspace: workspace.workspaceId, + path, + }); + const required = (flow.schema as any)?.required ?? []; + if (required.length > 0) { + throw new Error( + `Missing required arguments: ${required.join(", ")}.\nUse -d '{"${required[0]}": ...}' to provide input data.` + ); + } + } catch (e: any) { + if (e.message?.startsWith("Missing required")) throw e; + } + } + const id = await wmill.runFlowByPath({ workspace: workspace.workspaceId, path, @@ -467,6 +485,17 @@ async function preview( }); } catch (e: any) { if (e.body) { + // If a failure_module ran, the body contains its result — not an error + if (e.body.result !== undefined) { + if (opts.silent) { + console.log(JSON.stringify(e.body.result)); + } else { + log.info(colors.yellow.bold("Flow failed, error handler result:")); + log.info(JSON.stringify(e.body.result, null, 2)); + } + process.exitCode = 1; + return; + } log.error(`Flow preview failed: ${JSON.stringify(e.body)}`); } throw e; @@ -675,6 +704,7 @@ const command = new Command() "push a local flow spec. This overrides any remote versions." ) .arguments(" ") + .option("--message ", "Deployment message") .action(push as any) .command("run", "run a flow by path.") .arguments("") diff --git a/cli/src/commands/init/init.ts b/cli/src/commands/init/init.ts index dcd146b7e4..db4950575b 100644 --- a/cli/src/commands/init/init.ts +++ b/cli/src/commands/init/init.ts @@ -43,7 +43,7 @@ export interface InitOptions { */ async function initAction(opts: InitOptions) { if (await stat("wmill.yaml").catch(() => null)) { - log.error(colors.red("wmill.yaml already exists")); + log.info("wmill.yaml already exists, skipping config generation"); } else { // Detect current git branch for template const { isGitRepository, getCurrentGitBranch } = await import( diff --git a/cli/src/commands/script/script.ts b/cli/src/commands/script/script.ts index 41e1c16d76..2ff8822acc 100644 --- a/cli/src/commands/script/script.ts +++ b/cli/src/commands/script/script.ts @@ -104,7 +104,7 @@ export function isFlowInlineScriptPath(filePath: string): boolean { return isFlowInlineScriptPathInternal(filePath); } -type PushOptions = GlobalOptions; +type PushOptions = GlobalOptions & { message?: string }; async function push(opts: PushOptions, filePath: string) { opts = await mergeConfigWithConfigFile(opts); const workspace = await resolveWorkspace(opts); @@ -148,7 +148,7 @@ async function push(opts: PushOptions, filePath: string) { filePath, workspace, [], - undefined, + opts.message, opts, await getRawWorkspaceDependencies(true), codebases @@ -948,6 +948,25 @@ async function run( await requireLogin(opts); const input = opts.data ? await resolve(opts.data) : {}; + + // Validate required args against schema when no data provided + if (!opts.data) { + try { + const script = await wmill.getScriptByPath({ + workspace: workspace.workspaceId, + path, + }); + const required = (script.schema as any)?.required ?? []; + if (required.length > 0) { + throw new Error( + `Missing required arguments: ${required.join(", ")}.\nUse -d '{"${required[0]}": ...}' to provide input data.` + ); + } + } catch (e: any) { + if (e.message?.startsWith("Missing required")) throw e; + } + } + let id: string; try { id = await wmill.runScriptByPath({ @@ -1552,13 +1571,14 @@ async function history( return; } new Table() - .header(["#", "Hash", "Deployment Message"]) + .header(["#", "Hash", "Created At", "Deployment Message"]) .padding(2) .border(true) .body( versions.map((v, i) => [ String(versions.length - i), v.script_hash, + v.created_at ? new Date(v.created_at).toLocaleString() : "-", v.deployment_msg ?? "-", ]) ) @@ -1580,6 +1600,7 @@ const command = new Command() "push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh)" ) .arguments("") + .option("--message ", "Deployment message") .action(push as any) .command("get", "get a script's details") .arguments("") diff --git a/cli/src/guidance/skills.ts b/cli/src/guidance/skills.ts index 876383c96c..eaf28a1add 100644 --- a/cli/src/guidance/skills.ts +++ b/cli/src/guidance/skills.ts @@ -5058,6 +5058,7 @@ flow related commands - \`flow get \` - get a flow's details - \`--json\` - Output as JSON (for piping to jq) - \`flow push \` - push a local flow spec. This overrides any remote versions. + - \`--message \` - Deployment message - \`flow run \` - run a flow by path. - \`-d --data \` - Inputs specified as a JSON string or a file using @ or stdin using @-. - \`-s --silent\` - Do not ouput anything other then the final output. Useful for scripting. @@ -5336,6 +5337,7 @@ script related commands - \`--show-archived\` - Enable archived scripts in output - \`--json\` - Output as JSON (for piping to jq) - \`script push \` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh + - \`--message \` - Deployment message - \`script get \` - get a script's details - \`--json\` - Output as JSON (for piping to jq) - \`script show \` - show a script's content (alias for get diff --git a/frontend/src/lib/components/raw_apps/rawAppPolicy.ts b/frontend/src/lib/components/raw_apps/rawAppPolicy.ts index b90f1d3985..ec3e089745 100644 --- a/frontend/src/lib/components/raw_apps/rawAppPolicy.ts +++ b/frontend/src/lib/components/raw_apps/rawAppPolicy.ts @@ -1,18 +1,24 @@ import type { Policy, ScriptLang } from '$lib/gen' import { collectStaticFields, hash, type TriggerableV2 } from '../apps/editor/commonAppUtils' -import { isRunnableByName, isRunnableByPath, type InlineScript, type RunnableWithFields } from '../apps/inputType' +import { + isRunnableByName, + isRunnableByPath, + type InlineScript, + type RunnableWithFields +} from '../apps/inputType' export async function updateRawAppPolicy( runnables: Record, currentPolicy: Policy | undefined ): Promise { - const triggerables_v2 = Object.fromEntries( - (await Promise.all( + const entries = ( + await Promise.all( Object.entries(runnables).map(async ([id, runnable]) => { return await processRunnable(id, runnable, runnable?.fields ?? {}) }) - )) as [string, TriggerableV2][] - ) + ) + ).filter((entry): entry is [string, TriggerableV2] => entry != null) + const triggerables_v2 = Object.fromEntries(entries) return { ...currentPolicy, triggerables_v2 diff --git a/system_prompts/auto-generated/cli/cli-commands.md b/system_prompts/auto-generated/cli/cli-commands.md index 42f6343035..b9f23a76f0 100644 --- a/system_prompts/auto-generated/cli/cli-commands.md +++ b/system_prompts/auto-generated/cli/cli-commands.md @@ -100,6 +100,7 @@ flow related commands - `flow get ` - get a flow's details - `--json` - Output as JSON (for piping to jq) - `flow push ` - push a local flow spec. This overrides any remote versions. + - `--message ` - Deployment message - `flow run ` - run a flow by path. - `-d --data ` - Inputs specified as a JSON string or a file using @ or stdin using @-. - `-s --silent` - Do not ouput anything other then the final output. Useful for scripting. @@ -378,6 +379,7 @@ script related commands - `--show-archived` - Enable archived scripts in output - `--json` - Output as JSON (for piping to jq) - `script push ` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh + - `--message ` - Deployment message - `script get ` - get a script's details - `--json` - Output as JSON (for piping to jq) - `script show ` - show a script's content (alias for get diff --git a/system_prompts/auto-generated/prompts.ts b/system_prompts/auto-generated/prompts.ts index 0fe9b2b835..5d877702f4 100644 --- a/system_prompts/auto-generated/prompts.ts +++ b/system_prompts/auto-generated/prompts.ts @@ -1627,6 +1627,7 @@ flow related commands - \`flow get \` - get a flow's details - \`--json\` - Output as JSON (for piping to jq) - \`flow push \` - push a local flow spec. This overrides any remote versions. + - \`--message \` - Deployment message - \`flow run \` - run a flow by path. - \`-d --data \` - Inputs specified as a JSON string or a file using @ or stdin using @-. - \`-s --silent\` - Do not ouput anything other then the final output. Useful for scripting. @@ -1905,6 +1906,7 @@ script related commands - \`--show-archived\` - Enable archived scripts in output - \`--json\` - Output as JSON (for piping to jq) - \`script push \` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh + - \`--message \` - Deployment message - \`script get \` - get a script's details - \`--json\` - Output as JSON (for piping to jq) - \`script show \` - show a script's content (alias for get diff --git a/system_prompts/auto-generated/skills/cli-commands/SKILL.md b/system_prompts/auto-generated/skills/cli-commands/SKILL.md index 072c8d5330..a982abac90 100644 --- a/system_prompts/auto-generated/skills/cli-commands/SKILL.md +++ b/system_prompts/auto-generated/skills/cli-commands/SKILL.md @@ -105,6 +105,7 @@ flow related commands - `flow get ` - get a flow's details - `--json` - Output as JSON (for piping to jq) - `flow push ` - push a local flow spec. This overrides any remote versions. + - `--message ` - Deployment message - `flow run ` - run a flow by path. - `-d --data ` - Inputs specified as a JSON string or a file using @ or stdin using @-. - `-s --silent` - Do not ouput anything other then the final output. Useful for scripting. @@ -383,6 +384,7 @@ script related commands - `--show-archived` - Enable archived scripts in output - `--json` - Output as JSON (for piping to jq) - `script push ` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh + - `--message ` - Deployment message - `script get ` - get a script's details - `--json` - Output as JSON (for piping to jq) - `script show ` - show a script's content (alias for get