feat: script module mode with CLI sync, preview, and WAC UI improvements#8380
feat: script module mode with CLI sync, preview, and WAC UI improvements#8380rubenfiszel merged 30 commits intomainfrom
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add prominent comments explaining that all computation must happen inside task/step/taskScript or it will be replayed on resume/retry. Clarify that waitForApproval does not hold a worker and that approve/reject URLs are available in the timeline step details. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts: # frontend/package-lock.json # frontend/package.json
…nd preview support - Fix pull→push idempotency: use `??` instead of `||` for module lock field so empty strings are preserved (matches API's `lock: ""`) - Add per-module hash tracking in wmill-lock.yaml following the flow inline script pattern (SCRIPT_TOP_HASH + per-module subpath hashes) - Selective module lock regeneration: only regenerate locks for modules whose content actually changed, not all modules - Use unfiltered rawWorkspaceDependencies for module hashes to match what updateModuleLocks passes to fetchScriptLock - Show changed module names in stale script output for clarity - Add module support to `script preview` command: read modules from __mod/ folder and pass them in the preview API request - Add preview tests for taskScript pattern (flat and folder layout) - Update test assertion for module stale detection output Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ename, import consolidation - Reorder WAC template buttons: TypeScript before Python in ScriptBuilder, CreateActionsScript, and CreateActionsFlow - Remove dropdown items from +Script button (simplify to direct link) - Move "Import Workflow-as-Code" to +Flow dropdown with dedicated drawer - Add module tab rename: pencil icon on hover opens popover with validation, fixed-width icon container prevents layout shift Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolve conflicts: - cli/src/commands/script/script.ts: keep both isRawAppPath and module filters - frontend/package.json: keep local parser wasm paths - frontend/package-lock.json: regenerated from main + local deps - Restore backend/.sqlx cache from main - Add isScriptModulePath filter to unified generate-metadata command Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Backend parser updates for WAC detection - CLI sync/types updates for raw app path and module support - Frontend UI polish (Dev.svelte, ScriptRow, script hash page) - Test fixture updates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…adata Verifies that modifying a single module file re-triggers stale detection and only the changed module is listed, not all modules. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix hardcoded dev path in bun_executor.rs WAC v2 wrapper — use "windmill-client" import instead of absolute filesystem path - Fix missed no_main_func → auto_kind rename in parser TS test - Add modules column to clone_script SQL (windmill-common and windmill-api-workspaces) so cloned scripts retain their modules - Add modules: None to RawCode structs in worker tests - Restore complete sqlx cache (merge main's cache + our new queries) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change `.clone()` on double reference to `*k` dereference in scripts.rs hash implementation. Update sqlx cache with new query hashes from modified clone_script SQL. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The local file:// paths for windmill-parser-wasm-py and windmill-parser-wasm-ts don't exist in the Cloudflare Pages build environment. Revert to published npm versions (1.655.0). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use newly published windmill-parser-wasm-ts and windmill-parser-wasm-py v1.657.2 which include auto_kind/WAC detection changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ackages Regenerating package-lock.json from scratch pulled different dependency versions causing svelte-check type errors. Instead, start from main's lockfile and only update the two changed packages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Script<SR> struct has a modules field (FromRow), but fetch_script_for_update didn't SELECT modules, causing a runtime error "no column found for name: modules" when the worker processed dependency jobs. This was the root cause of the relock_skip test timeout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix modules not passed through job queue: inject _MODULES into PushArgs.extra when pushing Code jobs so worker can extract them - Fix Python module imports: use relative imports (from .helper) and add sys.path.insert for module directory in wrapper - Fix Python tests: use relative imports and empty lock to prevent pip from resolving module names as packages - Add local file check in Bun loader for module resolution - Ignore Bun module test (bundle mode loader integration tracked separately) - Add missing modules column to fetch_script_for_update query Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Relative imports (from .helper) are not parsed as pip packages, so the empty lock workaround is not needed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4209fa1 to
b67b56e
Compare
Python modules: - Use relative imports (from .helper import greet) since scripts run as packages - Add sys.path.insert for module directory in wrapper to ensure local modules take precedence over pip packages with same name Bun modules: - Use bundled output (./out/main.js) as wrapper import when modules are present — the bundled output has module content inlined by Bun.build, avoiding runtime loader resolution issues - Add local file check in loader.bun.js onResolve to short-circuit API URL resolution for module files on disk Job queue: - Inject _MODULES into PushArgs.extra when pushing Code jobs so the worker can extract them at execution time Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
b67b56e to
a54d7f9
Compare
…code
Critical fixes:
- Replace all CLI `no_main_func` references with `auto_kind` (string)
to match the backend migration and API changes
- Remove duplicated `compute_python_module_dir` in worker.rs, use
the canonical version from python_executor.rs
High priority:
- Auto-create `__init__.py` in intermediate directories for nested
Python modules so imports like `from .utils.math import add` work
without users manually creating __init__.py files
- Remove redundant `sys_path_insert` — relative imports use Python's
package system, not sys.path
Medium:
- Fix lock file base name extraction: use regex to strip only the
final extension (`.replace(/\.[^.]+$/, '')`) instead of `indexOf(".")`
which breaks for files like `helper.test.ts`
Simplification:
- Remove dead `{#if false}` Popover block in ScriptEditor.svelte
- Guard loader.bun.js local file check to only run for relative paths
(matching the Windows loader pattern)
- Add clarifying comment on Bun dual mechanism (build + run phases)
- Add maintenance comment on manual Hash impl for NewScript
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix sync.ts baseName extraction using indexOf(".") → regex
(same fix as script.ts/metadata.ts, missed this instance)
- Add stale module file cleanup in writeModulesToDisk: removes files
from __mod/ that are no longer in the modules map before writing,
fixing the pull→push cycle that couldn't delete modules
- Log warning when _MODULES serialization fails in job push instead
of silently dropping modules
- Use strict equality (===) for auto_kind comparison
- Exclude auto_kind from workspace export — it is auto-detected by
the parser at deploy time from script content
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
auto_kind is auto-detected by the parser at deploy time, so the CLI should not send it, compare it, or write it to script.yaml. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Claude finished @rubenfiszel's task in 8m 4s —— View job Review of PR #8380: Script Module Mode
This is a well-architected foundational feature. The backend implementation is solid — deterministic hashing, proper SQL parameterization, good error handling, and correct data flow through the stack. The CLI module sync with per-module hash tracking is a smart design. Below are the issues I found, organized by severity. Bugs1. When switching from a module tab back to the main script, function switchToMain() {
if (activeModuleTab !== null && modules) {
modules[activeModuleTab] = { ...modules[activeModuleTab], content: editorCode }
}
activeModuleTab = null
editorCode = code
+ lastSyncedCode = code // Missing — keep sync guard in sync
editor?.setCode(editorCode)
}2. When editing a module (activeModuleTab !== null), pressing format saves formatAction={async () => {
if (activeModuleTab === null) {
await inferSchema(editorCode)
}
try {
- localStorage.setItem(path ?? 'last_save', editorCode)
+ localStorage.setItem(path ?? 'last_save', activeModuleTab === null ? editorCode : code)
} catch (e) {3. Non-null assertion on const newLang = inferModuleLang(newPath)
modules[newPath] = { ...mod, language: newLang! } // ← force unwrapIf the user renames to an unsupported extension, modules[newPath] = { ...mod, language: newLang ?? mod.language }4. After deleting stale module files, the recursive 5. Redundant condition in if (activeModuleTab !== null && modules && activeModuleTab) {
// ^^^ already checked non-null ^^^ redundant truthy checkMinor, but the double check on Security — Defense in Depth6. Module let full_path = match base_dir {
Some(dir) => format!("{}/{}/{}", job_dir, dir, relpath),
None => format!("{}/{}", job_dir, relpath),
};Module paths ( let canonical = std::path::Path::new(&full_path).canonicalize()?;
if !canonical.starts_with(job_dir) {
return Err(Error::internal_err(format!("Invalid module path: {}", relpath)));
}Or simply reject paths containing Design Observations7. This exported function both flushes state AND resets the active tab. Parent components calling 8. The migration drops 9. WAC detection in frontend is heuristic-based — let isWacV2 = $derived.by(() => {
const isTsWac = mainCode.includes('windmill-client') && mainCode.includes('workflow') && mainCode.includes('task')This can false-positive on comments containing these words. The backend parser does proper AST-level analysis; the frontend check is just for UI gating (showing module tabs), so this is acceptable for now. 10. Module language inference ignores main script language —
Testing InstructionsTo verify the changes through the Windmill UI: Module Tab System: Navigate to the script editor (create a new TypeScript or Python script). Write a WAC-style script that imports from WAC Templates: On the home page, click "+Flow" — a modal should appear offering "Flow Editor" vs "Workflow as Code". Choose WAC TypeScript or Python to verify the template includes Script View with Modules: Navigate to a saved script that has modules (via WAC Export: On a WAC script's detail page, look for the "Export WAC" action to open a drawer that exports the script as YAML or JSON. WAC Import: From the "+Flow" dropdown, select "Import Workflow-as-Code" to open a drawer where you can paste YAML/JSON and import it as a new WAC script. |
Duplicate .sqlx cache was committed at the wrong nested path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
/ai-fast do full re-review of the PR |
|
Claude finished @rubenfiszel's task in 8m 15s —— View job Full Re-Review of PR #8380: Script Module Mode
Backend (Rust)Security1. Path traversal in let full_path = match base_dir {
Some(dir) => format!("{}/{}/{}", job_dir, dir, relpath),
None => format!("{}/{}", job_dir, relpath),
};A malicious module key like 2. No validation on 3. No size limits on modules (Low) Performance4. 5. Double Bug Risk6. Migration not idempotent (Low) 7. WAC scripts hidden from default listings — behavioral change (Medium) CLI (TypeScript)Security8. Path traversal in Bugs9. 10. Flat-layout scripts with JSON metadata not found by Minor11. 12. 13. Repeated lockfile read/write in loop (Low) — Frontend (Svelte)CLAUDE.md Compliance14. Banned pattern: Bugs15. 16. 17. No confirmation before deleting modules (Medium UX) Medium Issues18. Missing error handling in WAC import (Medium) 19. 20. 21. Module tab rename reorders tabs (Low UX) 22. Duplicated Summary
The two path traversal issues (#1 and #8) are the most important to address before merge — they allow writing files outside intended directories if module keys contain |
Frontend (ScriptEditor.svelte): - Fix switchToMain() missing lastSyncedCode update — prevents stale code sync on external changes while editing a module tab - Fix formatAction saving module code to main script's localStorage draft — now saves main code when on a module tab - Fix non-null assertion on inferModuleLang in renameModule — fall back to original language instead of force unwrap - Remove redundant activeModuleTab truthy check in runTest CLI (script.ts): - Clean up empty directories after removing stale module files in writeModulesToDisk Backend: - Add path traversal guard in write_module_files — reject module paths containing ".." - Fix dead_code warning on auto_kind field in workspace export struct Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ab5703f to
bd27a7d
Compare
|
/ai-fast do a full review |
|
Claude finished @rubenfiszel's task in 13m 26s —— View job Full Review of PR #8380: Script Module Mode
Overall this is a well-structured PR. The backend module system, deterministic hashing, WAC v2 execution model, and CLI per-module hash tracking are all solid. The path traversal fix in Medium Issues1. Every non-flow job (REST triggers, cron scripts, manual runs) now makes a DB query to let is_wac_replay = if job.auto_kind.as_deref() == Some("wac") {
if let Connection::Sql(db) = conn {
crate::wac_executor::load_checkpoint(db, &job.id)
.await
.map(|c| !c.completed_steps.is_empty())
.unwrap_or(false)
} else { false }
} else { false };2. if (k.startsWith(key)) {
delete conf.locks[k];
}
if (k === key || k.startsWith(key + "+")) {3. Missing path traversal validation in CLI The backend added for (const [relPath, mod] of Object.entries(modules)) {
const fullPath = path.join(moduleFolderPath, relPath);
// No validation that fullPath stays within moduleFolderPathAdd a check: if (relPath.includes('..')) {
log.warn(`Skipping module with path traversal: ${relPath}`);
continue;
}4. Missing try-catch in async function importWacRaw() {
const parsed =
wacImportType === 'yaml' ? YAML.parse(pendingWacRaw ?? '') : JSON.parse(pendingWacRaw ?? '')Invalid user input will throw an unhandled exception. Wrap in try-catch with a toast error message like other import handlers in the codebase. 5. WAC scripts hidden from default script listings — behavioral change — Changed from Low Issues6. CLAUDE.md compliance: modules = $bindable(undefined),Per CLAUDE.md, 7. No confirmation dialog before deleting modules —
8. Migration not idempotent — ALTER TABLE script ADD COLUMN auto_kind VARCHAR(20);Lacks 9. Double
10. No
Positive Observations
| Branch: |
- Rename "Include without main function" toggle to "Include library scripts" in script list (ItemsList.svelte) - Update NoMainFuncBadge: "No main" → "Library" with clearer tooltip - Filter module file extensions by main script language — Python scripts only allow .py modules, TypeScript only .ts, etc. - Split flushModuleState into flushModuleContent (no UI side-effect) and flushModuleState (flush + reset tab), reducing duplication - Dynamic placeholder and hint text in add module popover based on main script language Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
This PR introduces script module mode — a foundational feature that allows Windmill scripts to have companion module files, enabling the Workflow-as-Code (WAC) pattern where scripts can reference task modules via
taskScript("./helper.ts").Script Module System
__mod/folder, supporting two layouts:my_script.ts+my_script__mod/helper.tsmy_script__mod/script.ts+my_script__mod/helper.tsmodulesJSONB column on thescripttableCLI: Sync Pull/Push
readModulesFromDisk()/writeModulesToDisk()handle reading/writing module files from__mod/folderslock: lock ?? undefinedpreserves empty strings (API returnslock: ""for modules without deps)handleScriptMetadata()detects folder layout viaisModuleEntryPoint()CLI: Per-Module Hash Tracking
wmill-lock.yaml(following the flow inline script pattern)SCRIPT_TOP_HASHmeta-hash combines main script hash + sorted module hashescomputeModuleHashes()walks the__mod/folder and hashes each module's content + workspace depsupdateModuleLocks()accepts optionalchangedModulesparameter for selective lock regenerationf/test/my_script (bun) [changed modules: helper.ts]CLI: Preview with Modules
wmill script previewreads modules from__mod/and passes them in therunScriptPreviewAndWaitResultAPI callCLI: Unified generate-metadata
isScriptModulePath && !isModuleEntryPoint) so they're not treated as standalone scriptsBackend:
auto_kindRefactorno_main_func: bool→auto_kind: VARCHAR(20)on thescripttableNULL(normal script),"lib"(library/no main),"wac"(workflow-as-code)workflow()+task()patterns in TypeScript and Pythonno_main_func = truerows toauto_kind = 'lib'Backend: WAC v2 Execution
WorkflowCtx,StepSuspend,setWorkflowCtxfromwindmill-clientwmill.workflow()/wmill.task()decoratorsFrontend: WAC UI
task(),taskScript(),step(),sleep(),waitForApproval()w-[32px]) prevents layout shift on hoverKey Files Changed
windmill-types/src/scripts.rsauto_kindfield,ScriptModulestruct, hash implwindmill-api-scripts/src/scripts.rsauto_kindin create/updatebun_executor.rs,python_executor.rs,worker.rswindmill-parser-ts,windmill-parser-py20260313000000_script_auto_kind.up.sqlcli/src/commands/script/script.tsreadModulesFromDisk, preview, push/pullcli/src/utils/metadata.tscli/src/commands/generate-metadata/ScriptEditor.svelteScriptBuilder.svelteCreateActionsFlow.svelte,CreateActionsScript.svelteTest plan
bun test test/script_modules.test.ts— module sync unit testsbun test test/resource_folders_unit.test.ts— path utility testsbun test test/unified_generate_metadata.test.ts— metadata generation including module modification detectionbun test test/sync_pull_push.test.ts— sync round-trip testsbun test test/preview.test.ts— preview with modules (flat + folder layout)cargo check --features zipwith-D warnings— clean compilationsvelte-check— 0 errors in changed fileswmill sync pull→wmill sync pushproduces 0 changes for scripts with moduleswmill generate-metadataonly regenerates that module's lockwmill script previewworks with WAC scripts that have modules🤖 Generated with Claude Code