windmill-cli 1.614.0 → 1.615.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -107,3 +107,29 @@ To enable zsh completions add the following line to your `~/.zshrc`:
107
107
  ```
108
108
  source <(wmill completions zsh)
109
109
  ```
110
+
111
+ ## Development
112
+
113
+ ### Running Tests
114
+
115
+ **Prerequisites:**
116
+ - PostgreSQL running locally (default: `postgres://postgres:changeme@localhost:5432`)
117
+ - Rust toolchain installed
118
+
119
+ **Run tests locally (full features):**
120
+
121
+ ```bash
122
+ deno test --allow-all --no-check
123
+ ```
124
+
125
+ **Run tests in CI mode (minimal features, skips EE tests):**
126
+
127
+ ```bash
128
+ CI_MINIMAL_FEATURES=true deno test --allow-all --no-check
129
+ ```
130
+
131
+ | Variable | Description |
132
+ |----------|-------------|
133
+ | `CI_MINIMAL_FEATURES` | Set to `true` to skip EE-dependent tests |
134
+ | `DATABASE_URL` | PostgreSQL connection string |
135
+ | `EE_LICENSE_KEY` | Enterprise license key for EE features |
@@ -32,7 +32,7 @@ export const OpenAPI = {
32
32
  PASSWORD: undefined,
33
33
  TOKEN: getEnv("WM_TOKEN"),
34
34
  USERNAME: undefined,
35
- VERSION: '1.614.0',
35
+ VERSION: '1.615.0',
36
36
  WITH_CREDENTIALS: true,
37
37
  interceptors: {
38
38
  request: new Interceptors(),
@@ -6,6 +6,7 @@ import { colors, log, SEP, windmillUtils, yamlParseFile, yamlStringify, } from "
6
6
  import * as wmill from "../../../gen/services.gen.js";
7
7
  import path from "node:path";
8
8
  import { isSuperset } from "../../types.js";
9
+ import { deepEqual } from "../../utils/utils.js";
9
10
  import { replaceInlineScripts, repopulateFields } from "./app.js";
10
11
  import { createBundle, detectFrameworks } from "./bundle.js";
11
12
  import { APP_BACKEND_FOLDER } from "./app_metadata.js";
@@ -220,7 +221,7 @@ async function collectAppFiles(localPath) {
220
221
  entry.name === "sql_to_apply") {
221
222
  continue;
222
223
  }
223
- await readDirRecursive(fullPath + SEP, relativePath + SEP);
224
+ await readDirRecursive(fullPath + SEP, relativePath + "/");
224
225
  }
225
226
  else if (entry.isFile) {
226
227
  // Skip generated/metadata files that shouldn't be part of the app
@@ -296,7 +297,9 @@ export async function pushRawApp(workspace, remotePath, localPath, message) {
296
297
  log.info(colors.yellow.bold(`Creating raw app ${remotePath} bundle...`));
297
298
  // Detect frameworks to determine entry point
298
299
  const frameworks = detectFrameworks(localPath);
299
- const entryFile = frameworks.svelte || frameworks.vue ? "index.ts" : "index.tsx";
300
+ const entryFile = frameworks.svelte || frameworks.vue
301
+ ? "index.ts"
302
+ : "index.tsx";
300
303
  const entryPoint = localPath + entryFile;
301
304
  return await createBundle({
302
305
  entryPoint: entryPoint,
@@ -310,7 +313,11 @@ export async function pushRawApp(workspace, remotePath, localPath, message) {
310
313
  value.data = localApp.data;
311
314
  }
312
315
  if (app) {
313
- if (isSuperset({ ...localApp, runnables }, app)) {
316
+ // Check both metadata/runnables AND files for changes
317
+ // Files need separate comparison because isSuperset only checks if local keys exist in remote
318
+ const metadataUpToDate = isSuperset({ ...localApp, runnables }, app);
319
+ const filesUpToDate = deepEqual(files, app.value?.files);
320
+ if (metadataUpToDate && filesUpToDate) {
314
321
  log.info(colors.green(`App ${remotePath} is up to date`));
315
322
  return;
316
323
  }
@@ -326,7 +333,9 @@ export async function pushRawApp(workspace, remotePath, localPath, message) {
326
333
  summary: localApp.summary,
327
334
  policy: appForPolicy.policy,
328
335
  deployment_message: message,
329
- ...(localApp.custom_path ? { custom_path: localApp.custom_path } : {}),
336
+ ...(localApp.custom_path
337
+ ? { custom_path: localApp.custom_path }
338
+ : {}),
330
339
  },
331
340
  js,
332
341
  css,
@@ -344,7 +353,9 @@ export async function pushRawApp(workspace, remotePath, localPath, message) {
344
353
  summary: localApp.summary,
345
354
  policy: appForPolicy.policy,
346
355
  deployment_message: message,
347
- ...(localApp.custom_path ? { custom_path: localApp.custom_path } : {}),
356
+ ...(localApp.custom_path
357
+ ? { custom_path: localApp.custom_path }
358
+ : {}),
348
359
  },
349
360
  js,
350
361
  css,
@@ -14,7 +14,7 @@ export async function downloadZip(workspace, plainSecrets, skipVariables, skipRe
14
14
  const url = workspace.remote +
15
15
  "api/w/" +
16
16
  workspace.workspaceId +
17
- `/workspaces/tarball?archive_type=zip&plain_secret=${plainSecrets ?? false}&skip_variables=${skipVariables ?? false}&skip_resources=${skipResources ?? false}&skip_secrets=${skipSecrets ?? false}&include_schedules=${includeSchedules ?? false}&include_triggers=${includeTriggers ?? false}&include_users=${includeUsers ?? false}&include_groups=${includeGroups ?? false}&include_settings=${includeSettings ?? false}&include_key=${includeKey ?? false}&include_workspace_dependencies=${includeWorkspaceDependenciesValue}&default_ts=${defaultTs ?? "bun"}&skip_resource_types=${skipResourceTypes ?? false}`;
17
+ `/workspaces/tarball?archive_type=zip&plain_secret=${plainSecrets ?? false}&skip_variables=${skipVariables ?? false}&skip_resources=${skipResources ?? false}&skip_secrets=${skipSecrets ?? false}&include_schedules=${includeSchedules ?? false}&include_triggers=${includeTriggers ?? false}&include_users=${includeUsers ?? false}&include_groups=${includeGroups ?? false}&include_settings=${includeSettings ?? false}&include_key=${includeKey ?? false}&include_workspace_dependencies=${includeWorkspaceDependenciesValue}&default_ts=${defaultTs ?? "bun"}&skip_resource_types=${skipResourceTypes ?? false}&settings_version=v2`;
18
18
  const zipResponse = await fetch(url, {
19
19
  headers: requestHeaders,
20
20
  method: "GET",
@@ -2220,11 +2220,18 @@ export async function push(opts) {
2220
2220
  break;
2221
2221
  case "raw_app":
2222
2222
  if (isRawAppFolderMetadataFile(target)) {
2223
+ // Delete the entire raw app
2223
2224
  await wmill.deleteApp({
2224
2225
  workspace: workspaceId,
2225
2226
  path: removeSuffix(target, getDeleteSuffix("raw_app", "json")),
2226
2227
  });
2227
2228
  }
2229
+ else {
2230
+ // For individual file deletions within a raw app,
2231
+ // re-push the entire raw app so the backend gets the updated file list
2232
+ // (the deleted file won't be included in the push)
2233
+ await pushObj(workspaceId, target, undefined, undefined, opts.plainSecrets ?? false, alreadySynced, opts.message);
2234
+ }
2228
2235
  break;
2229
2236
  case "schedule":
2230
2237
  await wmill.deleteSchedule({
@@ -7,6 +7,67 @@ import { isSuperset } from "../types.js";
7
7
  import { deepEqual } from "../utils/utils.js";
8
8
  import { removeWorkerPrefix } from "../commands/worker-groups/worker-groups.js";
9
9
  import { decrypt, encrypt } from "../utils/local_encryption.js";
10
+ // Helper to convert legacy flat settings to new grouped format
11
+ export function migrateToGroupedFormat(settings) {
12
+ const result = { name: settings.name ?? "" };
13
+ // Copy non-legacy fields
14
+ if (settings.webhook !== undefined)
15
+ result.webhook = settings.webhook;
16
+ if (settings.deploy_to !== undefined)
17
+ result.deploy_to = settings.deploy_to;
18
+ if (settings.ai_config !== undefined)
19
+ result.ai_config = settings.ai_config;
20
+ if (settings.large_file_storage !== undefined)
21
+ result.large_file_storage = settings.large_file_storage;
22
+ if (settings.git_sync !== undefined)
23
+ result.git_sync = settings.git_sync;
24
+ if (settings.default_app !== undefined)
25
+ result.default_app = settings.default_app;
26
+ if (settings.default_scripts !== undefined)
27
+ result.default_scripts = settings.default_scripts;
28
+ if (settings.mute_critical_alerts !== undefined)
29
+ result.mute_critical_alerts = settings.mute_critical_alerts;
30
+ if (settings.color !== undefined)
31
+ result.color = settings.color;
32
+ if (settings.operator_settings !== undefined)
33
+ result.operator_settings = settings.operator_settings;
34
+ // Handle auto_invite: check if already grouped or needs migration
35
+ if (settings.auto_invite && typeof settings.auto_invite === "object") {
36
+ result.auto_invite = settings.auto_invite;
37
+ }
38
+ else if (settings.auto_invite_enabled !== undefined) {
39
+ // Legacy format
40
+ result.auto_invite = {
41
+ enabled: settings.auto_invite_enabled,
42
+ operator: settings.auto_invite_as === "operator",
43
+ mode: settings.auto_invite_mode ?? "invite",
44
+ };
45
+ }
46
+ // Handle error_handler: check if already grouped or needs migration
47
+ if (settings.error_handler && typeof settings.error_handler === "object") {
48
+ result.error_handler = settings.error_handler;
49
+ }
50
+ else if (typeof settings.error_handler === "string") {
51
+ // Legacy format (error_handler was a string path)
52
+ result.error_handler = {
53
+ path: settings.error_handler,
54
+ extra_args: settings.error_handler_extra_args,
55
+ muted_on_cancel: settings.error_handler_muted_on_cancel ?? false,
56
+ };
57
+ }
58
+ // Handle success_handler: check if already grouped or needs migration
59
+ if (settings.success_handler && typeof settings.success_handler === "object") {
60
+ result.success_handler = settings.success_handler;
61
+ }
62
+ else if (typeof settings.success_handler === "string") {
63
+ // Legacy format (success_handler was a string path)
64
+ result.success_handler = {
65
+ path: settings.success_handler,
66
+ extra_args: settings.success_handler_extra_args,
67
+ };
68
+ }
69
+ return result;
70
+ }
10
71
  const INSTANCE_SETTINGS_PATH = "instance_settings.yaml";
11
72
  let instanceSettingsPath = INSTANCE_SETTINGS_PATH;
12
73
  async function checkInstanceSettingsPath(opts) {
@@ -22,6 +83,8 @@ async function checkInstanceConfigPath(opts) {
22
83
  }
23
84
  }
24
85
  export async function pushWorkspaceSettings(workspace, _path, settings, localSettings) {
86
+ // Migrate local settings from legacy format if needed
87
+ localSettings = migrateToGroupedFormat(localSettings);
25
88
  try {
26
89
  const remoteSettings = await wmill.getSettings({
27
90
  workspace,
@@ -29,21 +92,13 @@ export async function pushWorkspaceSettings(workspace, _path, settings, localSet
29
92
  const workspaceName = await wmill.getWorkspaceName({
30
93
  workspace,
31
94
  });
95
+ // Build settings from remote (now using grouped format)
32
96
  settings = {
33
- // slack_team_id: remoteSettings.slack_team_id,
34
- // slack_name: remoteSettings.slack_name,
35
- // slack_command_script: remoteSettings.slack_command_script,
36
- // slack_email: remoteSettings.slack_email,
37
- auto_invite_enabled: remoteSettings.auto_invite_domain !== null,
38
- auto_invite_as: remoteSettings.auto_invite_operator
39
- ? "operator"
40
- : "developer",
41
- auto_invite_mode: remoteSettings.auto_add ? "add" : "invite",
97
+ auto_invite: remoteSettings.auto_invite,
98
+ error_handler: remoteSettings.error_handler,
99
+ success_handler: remoteSettings.success_handler,
42
100
  webhook: remoteSettings.webhook,
43
101
  deploy_to: remoteSettings.deploy_to,
44
- error_handler: remoteSettings.error_handler,
45
- error_handler_extra_args: remoteSettings.error_handler_extra_args,
46
- error_handler_muted_on_cancel: remoteSettings.error_handler_muted_on_cancel ?? false,
47
102
  ai_config: remoteSettings.ai_config,
48
103
  large_file_storage: remoteSettings.large_file_storage,
49
104
  git_sync: remoteSettings.git_sync,
@@ -64,7 +119,7 @@ export async function pushWorkspaceSettings(workspace, _path, settings, localSet
64
119
  }
65
120
  log.debug(`Workspace settings are not up-to-date, updating...`);
66
121
  if (localSettings.webhook !== settings.webhook) {
67
- log.debug(`Updateing webhook...`);
122
+ log.debug(`Updating webhook...`);
68
123
  await wmill.editWebhook({
69
124
  workspace,
70
125
  requestBody: {
@@ -72,24 +127,21 @@ export async function pushWorkspaceSettings(workspace, _path, settings, localSet
72
127
  },
73
128
  });
74
129
  }
75
- if (localSettings.auto_invite_as !== settings.auto_invite_as ||
76
- localSettings.auto_invite_enabled !== settings.auto_invite_enabled ||
77
- localSettings.auto_invite_mode !== settings.auto_invite_mode) {
130
+ // Handle auto_invite using grouped format
131
+ if (!deepEqual(localSettings.auto_invite, settings.auto_invite)) {
78
132
  log.debug(`Updating auto invite...`);
79
- if (!["operator", "developer"].includes(settings.auto_invite_as)) {
80
- throw new Error(`Invalid value for auto_invite_as. Valid values are "operator" and "developer"`);
81
- }
82
- if (!["add", "invite"].includes(settings.auto_invite_mode)) {
83
- throw new Error(`Invalid value for auto_invite_mode. Valid values are "invite" and "add"`);
133
+ const localAutoInvite = localSettings.auto_invite;
134
+ if (localAutoInvite?.mode && !["add", "invite"].includes(localAutoInvite.mode)) {
135
+ throw new Error(`Invalid value for auto_invite.mode. Valid values are "invite" and "add"`);
84
136
  }
85
137
  try {
86
138
  await wmill.editAutoInvite({
87
139
  workspace,
88
- requestBody: localSettings.auto_invite_enabled
140
+ requestBody: localAutoInvite?.enabled
89
141
  ? {
90
- operator: localSettings.auto_invite_as === "operator",
142
+ operator: localAutoInvite.operator ?? false,
91
143
  invite_all: true,
92
- auto_add: localSettings.auto_invite_mode === "add",
144
+ auto_add: localAutoInvite.mode === "add",
93
145
  }
94
146
  : {},
95
147
  });
@@ -99,11 +151,11 @@ export async function pushWorkspaceSettings(workspace, _path, settings, localSet
99
151
  log.debug(`Auto invite is not possible on cloud, only auto-inviting same domain...`);
100
152
  await wmill.editAutoInvite({
101
153
  workspace,
102
- requestBody: localSettings.auto_invite_enabled
154
+ requestBody: localAutoInvite?.enabled
103
155
  ? {
104
- operator: localSettings.auto_invite_as === "operator",
156
+ operator: localAutoInvite.operator ?? false,
105
157
  invite_all: false,
106
- auto_add: localSettings.auto_invite_mode === "add",
158
+ auto_add: localAutoInvite.mode === "add",
107
159
  }
108
160
  : {},
109
161
  });
@@ -116,17 +168,29 @@ export async function pushWorkspaceSettings(workspace, _path, settings, localSet
116
168
  requestBody: localSettings.ai_config ?? {},
117
169
  });
118
170
  }
119
- if (localSettings.error_handler != settings.error_handler ||
120
- !deepEqual(localSettings.error_handler_extra_args, settings.error_handler_extra_args) ||
121
- (localSettings.error_handler_muted_on_cancel ?? false) !=
122
- (settings.error_handler_muted_on_cancel ?? false)) {
171
+ // Handle error_handler using grouped format
172
+ if (!deepEqual(localSettings.error_handler, settings.error_handler)) {
123
173
  log.debug(`Updating error handler...`);
174
+ const localErrorHandler = localSettings.error_handler;
124
175
  await wmill.editErrorHandler({
125
176
  workspace,
126
177
  requestBody: {
127
- error_handler: localSettings.error_handler,
128
- error_handler_extra_args: localSettings.error_handler_extra_args,
129
- error_handler_muted_on_cancel: localSettings.error_handler_muted_on_cancel ?? false,
178
+ path: localErrorHandler?.path,
179
+ extra_args: localErrorHandler?.extra_args,
180
+ muted_on_cancel: localErrorHandler?.muted_on_cancel ?? false,
181
+ muted_on_user_path: localErrorHandler?.muted_on_user_path ?? false,
182
+ },
183
+ });
184
+ }
185
+ // Handle success_handler using grouped format
186
+ if (!deepEqual(localSettings.success_handler, settings.success_handler)) {
187
+ log.debug(`Updating success handler...`);
188
+ const localSuccessHandler = localSettings.success_handler;
189
+ await wmill.editSuccessHandler({
190
+ workspace,
191
+ requestBody: {
192
+ path: localSuccessHandler?.path,
193
+ extra_args: localSuccessHandler?.extra_args,
130
194
  },
131
195
  });
132
196
  }
@@ -200,7 +264,7 @@ export async function pushWorkspaceSettings(workspace, _path, settings, localSet
200
264
  },
201
265
  });
202
266
  }
203
- if (localSettings.operator_settings != settings.operator_settings) {
267
+ if (!deepEqual(localSettings.operator_settings, settings.operator_settings)) {
204
268
  log.debug(`Updating operator settings...`);
205
269
  await wmill.updateOperatorSettings({
206
270
  workspace,
package/esm/src/main.js CHANGED
@@ -40,7 +40,7 @@ export { flow, app, script, workspace, resource, resourceType, user, variable, h
40
40
  // console.error(JSON.stringify(event.error, null, 4));
41
41
  // }
42
42
  // });
43
- export const VERSION = "1.614.0";
43
+ export const VERSION = "1.615.0";
44
44
  // Re-exported from constants.ts to maintain backwards compatibility
45
45
  export { WM_FORK_PREFIX } from "./core/constants.js";
46
46
  const command = new Command()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "windmill-cli",
3
- "version": "1.614.0",
3
+ "version": "1.615.0",
4
4
  "description": "CLI for Windmill",
5
5
  "repository": {
6
6
  "type": "git",
@@ -730,6 +730,96 @@ export type FlowNote = {
730
730
  * Type of note - 'free' for standalone notes, 'group' for notes that group other nodes
731
731
  */
732
732
  export type type2 = 'free' | 'group';
733
+ /**
734
+ * Configuration for auto-inviting users to the workspace
735
+ */
736
+ export type AutoInviteConfig = {
737
+ enabled?: boolean;
738
+ domain?: string;
739
+ /**
740
+ * If true, auto-invited users are added as operators. If false, they are added as developers.
741
+ */
742
+ operator?: boolean;
743
+ mode?: 'invite' | 'add';
744
+ instance_groups?: Array<(string)>;
745
+ instance_groups_roles?: {
746
+ [key: string]: (string);
747
+ };
748
+ };
749
+ export type mode = 'invite' | 'add';
750
+ /**
751
+ * Configuration for the workspace error handler
752
+ */
753
+ export type ErrorHandlerConfig = {
754
+ /**
755
+ * Path to the error handler script or flow
756
+ */
757
+ path?: string;
758
+ extra_args?: ScriptArgs;
759
+ muted_on_cancel?: boolean;
760
+ muted_on_user_path?: boolean;
761
+ };
762
+ /**
763
+ * Configuration for the workspace success handler
764
+ */
765
+ export type SuccessHandlerConfig = {
766
+ /**
767
+ * Path to the success handler script or flow
768
+ */
769
+ path?: string;
770
+ extra_args?: ScriptArgs;
771
+ };
772
+ /**
773
+ * Request body for editing the workspace error handler. Accepts both new grouped format and legacy flat format for backward compatibility.
774
+ */
775
+ export type EditErrorHandler = EditErrorHandlerNew | EditErrorHandlerLegacy;
776
+ /**
777
+ * New grouped format for editing error handler
778
+ */
779
+ export type EditErrorHandlerNew = {
780
+ /**
781
+ * Path to the error handler script or flow
782
+ */
783
+ path?: string;
784
+ extra_args?: ScriptArgs;
785
+ muted_on_cancel?: boolean;
786
+ muted_on_user_path?: boolean;
787
+ };
788
+ /**
789
+ * Legacy flat format for editing error handler (deprecated, use new format)
790
+ */
791
+ export type EditErrorHandlerLegacy = {
792
+ /**
793
+ * Path to the error handler script or flow
794
+ */
795
+ error_handler?: string;
796
+ error_handler_extra_args?: ScriptArgs;
797
+ error_handler_muted_on_cancel?: boolean;
798
+ };
799
+ /**
800
+ * Request body for editing the workspace success handler. Accepts both new grouped format and legacy flat format for backward compatibility.
801
+ */
802
+ export type EditSuccessHandler = EditSuccessHandlerNew | EditSuccessHandlerLegacy;
803
+ /**
804
+ * New grouped format for editing success handler
805
+ */
806
+ export type EditSuccessHandlerNew = {
807
+ /**
808
+ * Path to the success handler script or flow
809
+ */
810
+ path?: string;
811
+ extra_args?: ScriptArgs;
812
+ };
813
+ /**
814
+ * Legacy flat format for editing success handler (deprecated, use new format)
815
+ */
816
+ export type EditSuccessHandlerLegacy = {
817
+ /**
818
+ * Path to the success handler script or flow
819
+ */
820
+ success_handler?: string;
821
+ success_handler_extra_args?: ScriptArgs;
822
+ };
733
823
  export type VaultSettings = {
734
824
  /**
735
825
  * HashiCorp Vault server address (e.g., https://vault.company.com:8200)
@@ -4064,23 +4154,14 @@ export type GetSettingsResponse = ({
4064
4154
  teams_command_script?: string;
4065
4155
  teams_team_name?: string;
4066
4156
  teams_team_guid?: string;
4067
- auto_invite_domain?: string;
4068
- auto_invite_operator?: boolean;
4069
- auto_add?: boolean;
4070
- auto_add_instance_groups?: Array<(string)>;
4071
- auto_add_instance_groups_roles?: {
4072
- [key: string]: (string);
4073
- };
4157
+ auto_invite?: AutoInviteConfig;
4074
4158
  plan?: string;
4075
4159
  customer_id?: string;
4076
4160
  webhook?: string;
4077
4161
  deploy_to?: string;
4078
4162
  ai_config?: AIConfig;
4079
- error_handler?: string;
4080
- error_handler_extra_args?: ScriptArgs;
4081
- error_handler_muted_on_cancel: boolean;
4082
- success_handler?: string;
4083
- success_handler_extra_args?: ScriptArgs;
4163
+ error_handler?: ErrorHandlerConfig;
4164
+ success_handler?: SuccessHandlerConfig;
4084
4165
  large_file_storage?: LargeFileStorage;
4085
4166
  ducklake?: DucklakeSettings;
4086
4167
  datatable?: DataTableSettings;
@@ -4343,11 +4424,7 @@ export type EditErrorHandlerData = {
4343
4424
  /**
4344
4425
  * WorkspaceErrorHandler
4345
4426
  */
4346
- requestBody: {
4347
- error_handler?: string;
4348
- error_handler_extra_args?: ScriptArgs;
4349
- error_handler_muted_on_cancel?: boolean;
4350
- };
4427
+ requestBody: EditErrorHandler;
4351
4428
  workspace: string;
4352
4429
  };
4353
4430
  export type EditErrorHandlerResponse = (string);
@@ -4355,10 +4432,7 @@ export type EditSuccessHandlerData = {
4355
4432
  /**
4356
4433
  * WorkspaceSuccessHandler
4357
4434
  */
4358
- requestBody: {
4359
- success_handler?: string;
4360
- success_handler_extra_args?: ScriptArgs;
4361
- };
4435
+ requestBody: EditSuccessHandler;
4362
4436
  workspace: string;
4363
4437
  };
4364
4438
  export type EditSuccessHandlerResponse = (string);