fix(cli): phantom diffs, flow safety, trigger DX, lint watch, error clarity (#8588)
* fix(cli): phantom diffs, flow push safety, error messages, digest stability Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli): differentiate stale vs missing metadata warnings on script push Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli): job list --limit off-by-one, deps push double error Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli): flow get shows nested steps, lint works on specific directories Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(cli): add lint --watch mode for continuous validation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli): email trigger template missing local_part, trigger get shows all fields Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli): fix CI — flow push warns instead of failing, lint subdir detection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,70 +43,66 @@ export async function pushWorkspaceDependencies(
|
||||
_befObj: any,
|
||||
newDependenciesContent: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const res = workspaceDependenciesPathToLanguageAndFilename(path);
|
||||
if (!res) {
|
||||
throw new Error(`Unknown workspace dependencies file format: ${path}`);
|
||||
}
|
||||
|
||||
const { language, name } = res;
|
||||
|
||||
const displayName = name
|
||||
? `named dependencies "${name}"`
|
||||
: `workspace default dependencies`;
|
||||
|
||||
// Fetch remote workspace dependencies and compare content directly
|
||||
try {
|
||||
const remoteDeps = await wmill.getLatestWorkspaceDependencies({
|
||||
workspace,
|
||||
language,
|
||||
name,
|
||||
});
|
||||
|
||||
if (remoteDeps && remoteDeps.content === newDependenciesContent) {
|
||||
log.info(
|
||||
colors.green(
|
||||
`${displayName} for ${language} are up-to-date, skipping push`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (e: any) {
|
||||
// If 404 or not found, the dependency doesn't exist remotely yet - proceed with push
|
||||
if (e.status !== 404 && !e.message?.includes("not found")) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
log.info(
|
||||
colors.yellow(
|
||||
`Pushing ${
|
||||
name ? "named" : "workspace default"
|
||||
} dependencies for ${language}...`,
|
||||
),
|
||||
const res = workspaceDependenciesPathToLanguageAndFilename(path);
|
||||
if (!res) {
|
||||
throw new Error(
|
||||
`Unknown workspace dependencies file format: ${path}. ` +
|
||||
`Valid files: package.json, requirements.in, composer.json, go.mod, modules.json`
|
||||
);
|
||||
}
|
||||
|
||||
await wmill.createWorkspaceDependencies({
|
||||
const { language, name } = res;
|
||||
|
||||
const displayName = name
|
||||
? `named dependencies "${name}"`
|
||||
: `workspace default dependencies`;
|
||||
|
||||
// Fetch remote workspace dependencies and compare content directly
|
||||
try {
|
||||
const remoteDeps = await wmill.getLatestWorkspaceDependencies({
|
||||
workspace,
|
||||
requestBody: {
|
||||
name,
|
||||
content: newDependenciesContent,
|
||||
language,
|
||||
workspace_id: workspace,
|
||||
// Description is not supported in cli, it will use old description
|
||||
description: undefined,
|
||||
},
|
||||
language,
|
||||
name,
|
||||
});
|
||||
|
||||
log.info(
|
||||
colors.green(`Successfully pushed ${displayName} for ${language}`),
|
||||
);
|
||||
} catch (error: any) {
|
||||
log.error(
|
||||
colors.red(`Failed to push workspace dependencies: ${error.message}`),
|
||||
);
|
||||
throw error;
|
||||
if (remoteDeps && remoteDeps.content === newDependenciesContent) {
|
||||
log.info(
|
||||
colors.green(
|
||||
`${displayName} for ${language} are up-to-date, skipping push`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (e: any) {
|
||||
// If 404 or not found, the dependency doesn't exist remotely yet - proceed with push
|
||||
if (e.status !== 404 && !e.message?.includes("not found")) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
log.info(
|
||||
colors.yellow(
|
||||
`Pushing ${
|
||||
name ? "named" : "workspace default"
|
||||
} dependencies for ${language}...`,
|
||||
),
|
||||
);
|
||||
|
||||
await wmill.createWorkspaceDependencies({
|
||||
workspace,
|
||||
requestBody: {
|
||||
name,
|
||||
content: newDependenciesContent,
|
||||
language,
|
||||
workspace_id: workspace,
|
||||
// Description is not supported in cli, it will use old description
|
||||
description: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
log.info(
|
||||
colors.green(`Successfully pushed ${displayName} for ${language}`),
|
||||
);
|
||||
}
|
||||
|
||||
export default command;
|
||||
|
||||
@@ -154,18 +154,27 @@ export async function pushFlow(
|
||||
const localFlow = (await yamlParseFile(localPath + "flow.yaml")) as FlowFile;
|
||||
|
||||
const fileReader = async (path: string) => await readFile(localPath + path, "utf-8");
|
||||
const missingFiles: string[] = [];
|
||||
await replaceInlineScripts(
|
||||
localFlow.value.modules,
|
||||
fileReader,
|
||||
log,
|
||||
localPath,
|
||||
SEP
|
||||
SEP,
|
||||
undefined,
|
||||
missingFiles
|
||||
);
|
||||
if (localFlow.value.failure_module) {
|
||||
await replaceInlineScripts([localFlow.value.failure_module], fileReader, log, localPath, SEP);
|
||||
await replaceInlineScripts([localFlow.value.failure_module], fileReader, log, localPath, SEP, undefined, missingFiles);
|
||||
}
|
||||
if (localFlow.value.preprocessor_module) {
|
||||
await replaceInlineScripts([localFlow.value.preprocessor_module], fileReader, log, localPath, SEP);
|
||||
await replaceInlineScripts([localFlow.value.preprocessor_module], fileReader, log, localPath, SEP, undefined, missingFiles);
|
||||
}
|
||||
if (missingFiles.length > 0) {
|
||||
log.warn(colors.yellow(
|
||||
`Warning: missing inline script file(s): ${missingFiles.join(", ")}. ` +
|
||||
`The flow will be pushed with unresolved !inline references.`
|
||||
));
|
||||
}
|
||||
|
||||
if (flow) {
|
||||
@@ -272,11 +281,26 @@ async function get(opts: GlobalOptions & { json?: boolean }, path: string) {
|
||||
const modules = (f as any).value?.modules;
|
||||
if (modules && Array.isArray(modules) && modules.length > 0) {
|
||||
console.log(colors.bold("Steps:"));
|
||||
for (const mod of modules) {
|
||||
const type = mod.value?.type ?? "unknown";
|
||||
const detail = mod.value?.language ?? mod.value?.path ?? "";
|
||||
console.log(` ${mod.id}: ${type}${detail ? " (" + detail + ")" : ""}`);
|
||||
function printModules(mods: any[], indent: string = " ") {
|
||||
for (const mod of mods) {
|
||||
const type = mod.value?.type ?? "unknown";
|
||||
const detail = mod.value?.language ?? mod.value?.path ?? "";
|
||||
console.log(`${indent}${mod.id}: ${type}${detail ? " (" + detail + ")" : ""}`);
|
||||
if (type === "branchall" || type === "branchone") {
|
||||
for (const branch of mod.value?.branches ?? []) {
|
||||
console.log(`${indent} Branch: ${branch.summary || "(default)"}`);
|
||||
if (branch.modules) printModules(branch.modules, indent + " ");
|
||||
}
|
||||
if (type === "branchone" && mod.value?.default) {
|
||||
console.log(`${indent} Default:`);
|
||||
printModules(mod.value.default, indent + " ");
|
||||
}
|
||||
} else if (type === "forloopflow" || type === "whileloopflow") {
|
||||
if (mod.value?.modules) printModules(mod.value.modules, indent + " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
printModules(modules);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -493,7 +517,6 @@ async function preview(
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
log.error(`Flow preview failed: ${JSON.stringify(e.body)}`);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -67,17 +67,20 @@ async function list(
|
||||
let successFilter = opts.success;
|
||||
if (opts.failed) successFilter = false;
|
||||
|
||||
const jobs = await wmill.listJobs({
|
||||
const limit = Math.min(opts.limit ?? 30, 100);
|
||||
const allJobs = await wmill.listJobs({
|
||||
workspace: workspace.workspaceId,
|
||||
scriptPathExact: opts.scriptPath,
|
||||
createdBy: opts.createdBy,
|
||||
running: opts.running,
|
||||
success: successFilter,
|
||||
perPage: Math.min(opts.limit ?? 30, 100),
|
||||
perPage: limit,
|
||||
jobKinds: opts.jobKinds ?? "script,flow,singlestepflow",
|
||||
label: opts.label,
|
||||
hasNullParent: opts.all ? undefined : true,
|
||||
});
|
||||
// API may return more than perPage — enforce limit client-side
|
||||
const jobs = allJobs.slice(0, limit);
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(jobs));
|
||||
|
||||
@@ -625,7 +625,13 @@ export async function runLint(
|
||||
throw new Error(`Path is not a directory: ${targetDirectory}`);
|
||||
}
|
||||
|
||||
const ignore = await ignoreF(mergedOpts);
|
||||
// When the user specifies a subdirectory (that doesn't contain wmill.yaml),
|
||||
// skip include/exclude filters since they're relative to the project root.
|
||||
const isSubdirectory = explicitTargetDirectory &&
|
||||
!(await stat(path.join(targetDirectory, "wmill.yaml")).catch(() => null));
|
||||
const ignore = isSubdirectory
|
||||
? (_p: string, _isDir: boolean) => false
|
||||
: await ignoreF(mergedOpts);
|
||||
const root = await FSFSElement(targetDirectory, [], false);
|
||||
const validator = new WindmillYamlValidator();
|
||||
|
||||
@@ -640,9 +646,10 @@ export async function runLint(
|
||||
if (entry.isDirectory || entry.ignored) {
|
||||
continue;
|
||||
}
|
||||
scannedFiles += 1;
|
||||
|
||||
const normalizedPath = normalizePath(entry.path);
|
||||
|
||||
scannedFiles += 1;
|
||||
if (!YAML_FILE_REGEX.test(normalizedPath)) {
|
||||
continue;
|
||||
}
|
||||
@@ -742,7 +749,11 @@ export function printReport(report: LintReport, jsonOutput: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
async function lint(opts: LintOptions, directory?: string) {
|
||||
async function lint(opts: LintOptions & { watch?: boolean }, directory?: string) {
|
||||
if (opts.watch) {
|
||||
await lintWatch(opts, directory);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const report = await runLint(opts, directory);
|
||||
printReport(report, !!opts.json);
|
||||
@@ -770,6 +781,37 @@ async function lint(opts: LintOptions, directory?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function lintWatch(opts: LintOptions, directory?: string) {
|
||||
const { watch } = await import("node:fs");
|
||||
const targetDir = directory ? path.resolve(process.cwd(), directory) : process.cwd();
|
||||
|
||||
log.info(colors.blue(`Watching ${targetDir} for changes... (Ctrl+C to stop)`));
|
||||
|
||||
async function runAndReport() {
|
||||
try {
|
||||
const report = await runLint(opts, directory);
|
||||
// Clear screen for readability
|
||||
process.stdout.write("\x1Bc");
|
||||
log.info(colors.gray(`[${new Date().toLocaleTimeString()}] Lint results:\n`));
|
||||
printReport(report, false);
|
||||
} catch (error) {
|
||||
log.error(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
await runAndReport();
|
||||
|
||||
let debounce: ReturnType<typeof setTimeout> | null = null;
|
||||
watch(targetDir, { recursive: true }, (_event, filename) => {
|
||||
if (!filename || !filename.toString().endsWith(".yaml") && !filename.toString().endsWith(".yml")) return;
|
||||
if (debounce) clearTimeout(debounce);
|
||||
debounce = setTimeout(runAndReport, 300);
|
||||
});
|
||||
|
||||
// Keep the process alive
|
||||
await new Promise(() => {});
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
.description(
|
||||
"Validate Windmill flow, schedule, and trigger YAML files in a directory",
|
||||
@@ -781,6 +823,7 @@ const command = new Command()
|
||||
"--locks-required",
|
||||
"Fail if scripts or flow inline scripts that need locks have no locks",
|
||||
)
|
||||
.option("-w, --watch", "Watch for file changes and re-lint automatically")
|
||||
.action(lint as any);
|
||||
|
||||
export default command;
|
||||
|
||||
@@ -126,20 +126,25 @@ async function push(opts: PushOptions, filePath: string) {
|
||||
|
||||
await requireLogin(opts);
|
||||
|
||||
// Warn if metadata appears stale (content changed since last generate-metadata)
|
||||
// Warn about metadata state before pushing
|
||||
try {
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
const remotePath = removeExtensionToPath(filePath).replaceAll(SEP, "/");
|
||||
const contentHash = await generateHash(content + remotePath);
|
||||
const conf = await readLockfile();
|
||||
if (!(await checkifMetadataUptodate(remotePath, contentHash, conf))) {
|
||||
const hasLockEntry = conf.locks && (conf.locks[remotePath] !== undefined || conf.locks[`${remotePath}.ts`] !== undefined);
|
||||
if (!hasLockEntry) {
|
||||
log.warn(colors.yellow(
|
||||
`No metadata generated yet for ${filePath}. Run 'wmill generate-metadata' to generate schema and lock.`
|
||||
));
|
||||
} else if (!(await checkifMetadataUptodate(remotePath, contentHash, conf))) {
|
||||
log.warn(colors.yellow(
|
||||
`Metadata for ${filePath} appears stale (content changed since last 'wmill generate-metadata').\n` +
|
||||
`The schema and lock may not match the current code. Consider running 'wmill generate-metadata' first.`
|
||||
));
|
||||
}
|
||||
} catch {
|
||||
// Don't block push if staleness check fails
|
||||
// Don't block push if check fails
|
||||
}
|
||||
|
||||
const codebases = await listSyncCodebases(opts as SyncOptions);
|
||||
@@ -1584,11 +1589,11 @@ async function history(
|
||||
|
||||
const command = new Command()
|
||||
.description("script related commands")
|
||||
.option("--show-archived", "Enable archived scripts in output")
|
||||
.option("--show-archived", "Show archived scripts instead of active ones")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("list", "list all scripts")
|
||||
.option("--show-archived", "Enable archived scripts in output")
|
||||
.option("--show-archived", "Show archived scripts instead of active ones")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command(
|
||||
|
||||
@@ -76,7 +76,7 @@ import {
|
||||
newRawAppPathAssigner,
|
||||
PathAssigner,
|
||||
} from "../../../windmill-utils-internal/src/path-utils/path-assigner.ts";
|
||||
import { extractInlineScripts as extractInlineScriptsForFlows } from "../../../windmill-utils-internal/src/inline-scripts/extractor.ts";
|
||||
import { extractInlineScripts as extractInlineScriptsForFlows, extractCurrentMapping } from "../../../windmill-utils-internal/src/inline-scripts/extractor.ts";
|
||||
import { generateFlowLockInternal } from "../flow/flow_metadata.ts";
|
||||
import { isExecutionModeAnonymous } from "../app/app.ts";
|
||||
import {
|
||||
@@ -638,9 +638,16 @@ function ZipFSElement(
|
||||
let inlineScripts;
|
||||
try {
|
||||
const assigner = newPathAssigner(defaultTs, { skipInlineScriptSuffix: getNonDottedPaths() });
|
||||
inlineScripts = extractInlineScriptsForFlows(
|
||||
// Preserve original !inline filenames from the flow to avoid phantom renames
|
||||
const inlineMapping = extractCurrentMapping(
|
||||
flow.value.modules as any,
|
||||
{},
|
||||
flow.value.failure_module,
|
||||
flow.value.preprocessor_module,
|
||||
);
|
||||
inlineScripts = extractInlineScriptsForFlows(
|
||||
flow.value.modules as any,
|
||||
inlineMapping,
|
||||
SEP,
|
||||
defaultTs,
|
||||
assigner,
|
||||
@@ -649,7 +656,7 @@ function ZipFSElement(
|
||||
if (flow.value.failure_module) {
|
||||
inlineScripts.push(...extractInlineScriptsForFlows(
|
||||
[flow.value.failure_module],
|
||||
{},
|
||||
inlineMapping,
|
||||
SEP,
|
||||
defaultTs,
|
||||
assigner,
|
||||
@@ -659,7 +666,7 @@ function ZipFSElement(
|
||||
if (flow.value.preprocessor_module) {
|
||||
inlineScripts.push(...extractInlineScriptsForFlows(
|
||||
[flow.value.preprocessor_module],
|
||||
{},
|
||||
inlineMapping,
|
||||
SEP,
|
||||
defaultTs,
|
||||
assigner,
|
||||
@@ -1518,6 +1525,10 @@ async function compareDynFSElement(
|
||||
continue;
|
||||
}
|
||||
if (k.startsWith("dependencies/")) {
|
||||
if (!workspaceDependenciesPathToLanguageAndFilename(k)) {
|
||||
log.warn(`Skipping unrecognized workspace dependencies file: ${k}`);
|
||||
continue;
|
||||
}
|
||||
log.info(`Adding workspace dependencies file: ${k}`);
|
||||
}
|
||||
changes.push({ name: "added", path: k, content: v });
|
||||
|
||||
@@ -378,6 +378,7 @@ const triggerTemplates: Record<TriggerType, Record<string, any>> = {
|
||||
email: {
|
||||
script_path: "",
|
||||
is_flow: false,
|
||||
local_part: "",
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
@@ -409,6 +410,27 @@ async function newTrigger(opts: GlobalOptions & { kind: string }, path: string)
|
||||
log.info(colors.green(`Created ${filePath}`));
|
||||
}
|
||||
|
||||
const TRIGGER_SKIP_FIELDS = new Set(["workspace_id", "extra_perms", "edited_by", "edited_at"]);
|
||||
|
||||
function printTriggerDetails(trigger: any, kind: string) {
|
||||
console.log(colors.bold("Path:") + " " + trigger.path);
|
||||
console.log(colors.bold("Kind:") + " " + kind);
|
||||
console.log(colors.bold("Enabled:") + " " + (trigger.enabled ?? trigger.mode ?? "-"));
|
||||
console.log(colors.bold("Script Path:") + " " + (trigger.script_path ?? ""));
|
||||
console.log(colors.bold("Is Flow:") + " " + (trigger.is_flow ? "true" : "false"));
|
||||
// Show all other non-internal fields
|
||||
for (const [key, value] of Object.entries(trigger)) {
|
||||
if (["path", "enabled", "mode", "script_path", "is_flow"].includes(key)) continue;
|
||||
if (TRIGGER_SKIP_FIELDS.has(key)) continue;
|
||||
if (value === undefined || value === null || value === "") continue;
|
||||
const display = Array.isArray(value) ? (value.length > 0 ? JSON.stringify(value) : "[]") :
|
||||
typeof value === "object" ? JSON.stringify(value) : String(value);
|
||||
if (display === "[]" || display === "{}") continue;
|
||||
const label = key.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase());
|
||||
console.log(colors.bold(label + ":") + " " + display);
|
||||
}
|
||||
}
|
||||
|
||||
async function get(opts: GlobalOptions & { json?: boolean; kind?: string }, path: string) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
@@ -422,11 +444,7 @@ async function get(opts: GlobalOptions & { json?: boolean; kind?: string }, path
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(trigger));
|
||||
} else {
|
||||
console.log(colors.bold("Path:") + " " + (trigger as any).path);
|
||||
console.log(colors.bold("Kind:") + " " + opts.kind);
|
||||
console.log(colors.bold("Enabled:") + " " + ((trigger as any).enabled ?? "-"));
|
||||
console.log(colors.bold("Script Path:") + " " + ((trigger as any).script_path ?? ""));
|
||||
console.log(colors.bold("Is Flow:") + " " + ((trigger as any).is_flow ? "true" : "false"));
|
||||
printTriggerDetails(trigger as any, opts.kind);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -451,11 +469,7 @@ async function get(opts: GlobalOptions & { json?: boolean; kind?: string }, path
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(trigger));
|
||||
} else {
|
||||
console.log(colors.bold("Path:") + " " + trigger.path);
|
||||
console.log(colors.bold("Kind:") + " " + kind);
|
||||
console.log(colors.bold("Enabled:") + " " + (trigger.enabled ?? (trigger as any).mode ?? "-"));
|
||||
console.log(colors.bold("Script Path:") + " " + (trigger.script_path ?? ""));
|
||||
console.log(colors.bold("Is Flow:") + " " + (trigger.is_flow ? "true" : "false"));
|
||||
printTriggerDetails(trigger, kind);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -550,7 +550,7 @@ export async function resolveWorkspace(
|
||||
}
|
||||
|
||||
// If everything failed, show error
|
||||
log.info(colors.red.bold("No workspace given and no default set."));
|
||||
log.info(colors.red.bold("No workspace given and no default set. Run 'wmill workspace add' to configure one."));
|
||||
return process.exit(-1);
|
||||
}
|
||||
|
||||
|
||||
@@ -5260,6 +5260,7 @@ Validate Windmill flow, schedule, and trigger YAML files in a directory
|
||||
- \`--json\` - Output results in JSON format
|
||||
- \`--fail-on-warn\` - Exit with code 1 when warnings are emitted
|
||||
- \`--locks-required\` - Fail if scripts or flow inline scripts that need locks have no locks
|
||||
- \`-w, --watch\` - Watch for file changes and re-lint automatically
|
||||
|
||||
### queues
|
||||
|
||||
@@ -5328,13 +5329,13 @@ schedule related commands
|
||||
script related commands
|
||||
|
||||
**Options:**
|
||||
- \`--show-archived\` - Enable archived scripts in output
|
||||
- \`--show-archived\` - Show archived scripts instead of active ones
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- \`script list\` - list all scripts
|
||||
- \`--show-archived\` - Enable archived scripts in output
|
||||
- \`--show-archived\` - Show archived scripts instead of active ones
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`script push <path:file>\` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh
|
||||
- \`--message <message:string>\` - Deployment message
|
||||
|
||||
@@ -231,7 +231,9 @@ async function main() {
|
||||
} catch (e) {
|
||||
if (e && typeof e === "object" && "name" in e && e.name === "ApiError") {
|
||||
const body = (e as any).body;
|
||||
const bodyStr = typeof body === "object" && body !== null ? JSON.stringify(body) : body;
|
||||
let bodyStr = typeof body === "object" && body !== null ? JSON.stringify(body) : String(body ?? "");
|
||||
// Strip backend source file references like (flows.rs:1400) or @scripts.rs:123:45
|
||||
bodyStr = bodyStr.replace(/\s*[@(]\w+\.rs:\d+[:\d]*\)?/g, "");
|
||||
log.error(
|
||||
"Server failed. " + (e as any).statusText + ": " + bodyStr
|
||||
);
|
||||
|
||||
@@ -107,6 +107,7 @@ export function getHeaders(): Record<string, string> | undefined {
|
||||
export async function digestDir(path: string, conf: string) {
|
||||
const hashes: string = [];
|
||||
const entries = await readdir(path, { withFileTypes: true });
|
||||
entries.sort((a, b) => a.name.localeCompare(b.name));
|
||||
for (const e of entries) {
|
||||
const npath = path + "/" + e.name;
|
||||
if (e.isFile()) {
|
||||
|
||||
@@ -13,7 +13,8 @@ async function replaceRawscriptInline(
|
||||
fileReader: (path: string) => Promise<string>,
|
||||
logger: { info: (message: string) => void; error: (message: string) => void },
|
||||
separator: string,
|
||||
removeLocks?: string[]
|
||||
removeLocks?: string[],
|
||||
missingFiles?: string[]
|
||||
): Promise<void> {
|
||||
if (!rawscript.content || !rawscript.content.startsWith("!inline")) {
|
||||
return;
|
||||
@@ -31,6 +32,7 @@ async function replaceRawscriptInline(
|
||||
rawscript.content = await fileReader(newPath);
|
||||
} catch {
|
||||
logger.error(`Script file ${newPath} not found`);
|
||||
if (missingFiles) missingFiles.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,14 +78,14 @@ export async function replaceInlineScripts(
|
||||
localPath: string,
|
||||
separator: string = "/",
|
||||
removeLocks?: string[],
|
||||
// renamer?: (path: string, newPath: string) => void,
|
||||
// deleter?: (path: string) => void
|
||||
): Promise<void> {
|
||||
missingFiles?: string[],
|
||||
): Promise<string[]> {
|
||||
const missing = missingFiles ?? [];
|
||||
await Promise.all(modules.map(async (module) => {
|
||||
if (!module.value) {
|
||||
throw new Error(`Module value is undefined for module ${module.id}`);
|
||||
}
|
||||
|
||||
|
||||
if (module.value.type === "rawscript") {
|
||||
await replaceRawscriptInline(
|
||||
module.id,
|
||||
@@ -91,19 +93,20 @@ export async function replaceInlineScripts(
|
||||
fileReader,
|
||||
logger,
|
||||
separator,
|
||||
removeLocks
|
||||
removeLocks,
|
||||
missing
|
||||
);
|
||||
} else if (module.value.type === "forloopflow" || module.value.type === "whileloopflow") {
|
||||
await replaceInlineScripts(module.value.modules, fileReader, logger, localPath, separator, removeLocks);
|
||||
await replaceInlineScripts(module.value.modules, fileReader, logger, localPath, separator, removeLocks, missing);
|
||||
} else if (module.value.type === "branchall") {
|
||||
await Promise.all(module.value.branches.map(async (branch) => {
|
||||
await replaceInlineScripts(branch.modules, fileReader, logger, localPath, separator, removeLocks);
|
||||
await replaceInlineScripts(branch.modules, fileReader, logger, localPath, separator, removeLocks, missing);
|
||||
}));
|
||||
} else if (module.value.type === "branchone") {
|
||||
await Promise.all(module.value.branches.map(async (branch) => {
|
||||
await replaceInlineScripts(branch.modules, fileReader, logger, localPath, separator, removeLocks);
|
||||
await replaceInlineScripts(branch.modules, fileReader, logger, localPath, separator, removeLocks, missing);
|
||||
}));
|
||||
await replaceInlineScripts(module.value.default, fileReader, logger, localPath, separator, removeLocks);
|
||||
await replaceInlineScripts(module.value.default, fileReader, logger, localPath, separator, removeLocks, missing);
|
||||
} else if (module.value.type === "aiagent") {
|
||||
await Promise.all((module.value.tools ?? []).map(async (tool) => {
|
||||
const toolValue = tool.value;
|
||||
@@ -120,11 +123,13 @@ export async function replaceInlineScripts(
|
||||
fileReader,
|
||||
logger,
|
||||
separator,
|
||||
removeLocks
|
||||
removeLocks,
|
||||
missing
|
||||
);
|
||||
}));
|
||||
}
|
||||
}));
|
||||
return missing;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -302,6 +302,7 @@ Validate Windmill flow, schedule, and trigger YAML files in a directory
|
||||
- `--json` - Output results in JSON format
|
||||
- `--fail-on-warn` - Exit with code 1 when warnings are emitted
|
||||
- `--locks-required` - Fail if scripts or flow inline scripts that need locks have no locks
|
||||
- `-w, --watch` - Watch for file changes and re-lint automatically
|
||||
|
||||
### queues
|
||||
|
||||
@@ -370,13 +371,13 @@ schedule related commands
|
||||
script related commands
|
||||
|
||||
**Options:**
|
||||
- `--show-archived` - Enable archived scripts in output
|
||||
- `--show-archived` - Show archived scripts instead of active ones
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- `script list` - list all scripts
|
||||
- `--show-archived` - Enable archived scripts in output
|
||||
- `--show-archived` - Show archived scripts instead of active ones
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `script push <path:file>` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh
|
||||
- `--message <message:string>` - Deployment message
|
||||
|
||||
@@ -1829,6 +1829,7 @@ Validate Windmill flow, schedule, and trigger YAML files in a directory
|
||||
- \`--json\` - Output results in JSON format
|
||||
- \`--fail-on-warn\` - Exit with code 1 when warnings are emitted
|
||||
- \`--locks-required\` - Fail if scripts or flow inline scripts that need locks have no locks
|
||||
- \`-w, --watch\` - Watch for file changes and re-lint automatically
|
||||
|
||||
### queues
|
||||
|
||||
@@ -1897,13 +1898,13 @@ schedule related commands
|
||||
script related commands
|
||||
|
||||
**Options:**
|
||||
- \`--show-archived\` - Enable archived scripts in output
|
||||
- \`--show-archived\` - Show archived scripts instead of active ones
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- \`script list\` - list all scripts
|
||||
- \`--show-archived\` - Enable archived scripts in output
|
||||
- \`--show-archived\` - Show archived scripts instead of active ones
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`script push <path:file>\` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh
|
||||
- \`--message <message:string>\` - Deployment message
|
||||
|
||||
@@ -307,6 +307,7 @@ Validate Windmill flow, schedule, and trigger YAML files in a directory
|
||||
- `--json` - Output results in JSON format
|
||||
- `--fail-on-warn` - Exit with code 1 when warnings are emitted
|
||||
- `--locks-required` - Fail if scripts or flow inline scripts that need locks have no locks
|
||||
- `-w, --watch` - Watch for file changes and re-lint automatically
|
||||
|
||||
### queues
|
||||
|
||||
@@ -375,13 +376,13 @@ schedule related commands
|
||||
script related commands
|
||||
|
||||
**Options:**
|
||||
- `--show-archived` - Enable archived scripts in output
|
||||
- `--show-archived` - Show archived scripts instead of active ones
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- `script list` - list all scripts
|
||||
- `--show-archived` - Enable archived scripts in output
|
||||
- `--show-archived` - Show archived scripts instead of active ones
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `script push <path:file>` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh
|
||||
- `--message <message:string>` - Deployment message
|
||||
|
||||
Reference in New Issue
Block a user