Compare commits

..

95 Commits

Author SHA1 Message Date
wendrul
3cb5660057 Windmill debugger binary 2025-07-30 19:08:25 +02:00
Ruben Fiszel
25aa982318 nit naming 2025-07-30 12:58:33 +00:00
Ruben Fiszel
4b9683f146 feat: prevent too large results (>500Mb) from OOMing database 2025-07-30 12:55:39 +00:00
Ruben Fiszel
14aa6d9681 add more tracing logs 2025-07-30 10:00:48 +00:00
Ruben Fiszel
2619be679e even more indexer tracings 2025-07-30 07:55:19 +00:00
Ruben Fiszel
6971dcdb05 better indexer tracings 2025-07-30 07:25:16 +00:00
Ruben Fiszel
b0616c74cf chore(main): release 1.515.1 (#6295)
* chore(main): release 1.515.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-29 22:21:14 +00:00
Ruben Fiszel
daf3bb0a0a nits logs 2025-07-29 21:01:38 +00:00
Ruben Fiszel
2e7ab919a7 fix: improved logs for script 2025-07-29 20:51:43 +00:00
Ruben Fiszel
b5afff9cc1 chore(main): release 1.515.0 (#6292)
* chore(main): release 1.515.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-29 19:41:05 +00:00
centdix
29c686d62a feat(cli): generate cursor rules on init (#6270)
* create cursor rules on init

* change gen

* add missing resource-type command

* add resource type command in guidance

* add schema option

* revert

* nit

* nit

* add flow guidance

* nit
2025-07-29 19:35:55 +00:00
Diego Imbert
5af47d29e7 Small UI fixes (#6294)
* fix step history not refreshing with staticInputs

* fix array of obj not showing up in json editor in test this step

* datatable scales correctly in DisplayResult and scrolling is much more usable

* avoid next button disapearing and changing layout / hurting ux

* nits

* fix bug when renaming module A to B then module C to A, C takes the schema of A

* fix bug with comments in sql repl

* fix aggrid theme randomly not loading

* bindable script

* better delete button in db manager

* property select doesnt exist

* fix all warnings

* delete $flowStateStore[id] on delete
2025-07-29 19:28:54 +00:00
wendrul
4d6a61403b fix: add size limit to indexer queries on jobs table to avoid oom (#6293)
* Update nix flake

* Update ee repo ref

* update ee-repo-ref

* Move collect stream with limits util to common

* add ee-repo-ref

* update ee-repo-ref

* update eereporef
2025-07-29 18:27:59 +00:00
wendrul
d5c21131df Update nix flake (#6291) 2025-07-29 16:26:38 +00:00
Ruben Fiszel
56671bc75f fix: resource types as arg in typescript handle imported defined types 2025-07-29 16:01:28 +00:00
dieriba
9931311650 fix: resource-type-ts-parser (#6289) 2025-07-29 14:53:24 +00:00
Ruben Fiszel
379880c6c5 nits 2025-07-29 14:51:14 +00:00
Ruben Fiszel
55ba599022 fix: fix DynSelect 2025-07-29 14:33:14 +00:00
Ruben Fiszel
8a2cd1c468 fix import nit 2025-07-29 14:26:55 +00:00
Ruben Fiszel
1084777bd6 support $res: string in form inputs of arrays 2025-07-29 12:40:37 +00:00
Ruben Fiszel
ce4177ebae fix: improve docker logs collection in docker mode 2025-07-28 23:48:28 +00:00
Ruben Fiszel
454b258ab8 chore(main): release 1.514.1 (#6288)
* chore(main): release 1.514.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-28 15:48:28 +00:00
Ruben Fiszel
adf59a36a9 pin rustls to 0.23.29 + pin aws-sdk 2025-07-28 15:18:54 +00:00
Ruben Fiszel
1a85dc7008 fix: pin tokio to 1.46.1 and aws-sdks-ts 2025-07-28 14:49:29 +00:00
Ruben Fiszel
496bbeec36 chore(main): release 1.514.0 (#6283)
* chore(main): release 1.514.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-28 12:30:26 +02:00
centdix
f892f7c9af fix(mcp): add proper check for mcp routes (#6282)
* add proper check for mcp routes

* cleaner

* apply to flow

* fix add checks scopes

---------

Co-authored-by: dieriba <dieriba.pro@gmail.com>
2025-07-28 10:11:36 +00:00
Ruben Fiszel
6e8f5a5b00 feat: migrate audit log ids to bigints (blocking migration for EE) 2025-07-28 08:52:39 +00:00
centdix
d09d90589d remove duplicate caching (#6285) 2025-07-28 08:01:22 +00:00
Ruben Fiszel
a999bc7b28 fix: fix id renaming in apps 2025-07-27 23:15:19 +00:00
Ruben Fiszel
564e92e98d cleanup warnings 2025-07-26 13:48:53 +00:00
Ruben Fiszel
323ec9a994 remove warnings 2025-07-26 13:28:51 +00:00
dieriba
ad441a7ba6 improve: add retry logic for sqs oidc fetch credentials (#6275)
* improve

* update dependency

* push ee ref

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-07-26 13:25:25 +00:00
Ruben Fiszel
c067342369 chore(main): release 1.513.1 (#6272)
* chore(main): release 1.513.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-25 18:11:43 +00:00
centdix
e8f4c2418b fix(mcp): allow mcp scopes to run scripts and flows (#6278)
* allow mcp scopes to run scripts and flows

* fix

* fix
2025-07-25 17:16:03 +00:00
David P. Kleinschmidt
359b34ef08 Use backwards-compatible union type syntax in Python client (#6277) 2025-07-25 15:25:08 +00:00
Ruben Fiszel
8b4dc2ce73 nit flow logs message with error handler 2025-07-25 14:06:53 +00:00
Ruben Fiszel
3f784e3c07 fix: improve error handler behavior wrt to parrallel branchall & forloops (#6273)
* error handler improvement

* fix: improve error handler behavior with parallel for loops

* all

* Error handler
2025-07-25 13:53:43 +00:00
Ruben Fiszel
94e20a97e0 fix: jobLoader for flowstatusviewerinner work with public apps 2025-07-25 09:44:07 +00:00
Ruben Fiszel
06bd89c957 chore(main): release 1.513.0 (#6269)
* chore(main): release 1.513.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-24 23:24:00 +00:00
Ruben Fiszel
241be4897e fix: fix webhook panel reactivity 2025-07-24 23:20:48 +00:00
Ruben Fiszel
9f926bd6bb fix backend tests 2025-07-24 21:21:25 +00:00
hugocasa
79205839b6 fix: add windir env var to pwsh for legacy modules like AD (#6271) 2025-07-24 17:38:26 +00:00
Ruben Fiszel
ed1bee9f66 nit 2025-07-24 17:17:59 +00:00
Ruben Fiszel
f71104e9f6 fix: improve aggrid columnDefs reactivity 2025-07-24 17:11:36 +00:00
Ruben Fiszel
e16c8d0199 fix warning 2025-07-24 12:16:14 +00:00
iqdecay
be3173d048 feat: enable workspace exclusion in custom tags (#6263)
* feat: enable tags that exclude workspaces

* feat: frontend tooltip

* fix: use method to check if tag applies

* refactor: change conversion method

* fix: change operator comparison

* remove redundant tests
2025-07-24 12:11:29 +00:00
Ruben Fiszel
f4073abef0 test fix 2025-07-24 12:08:20 +00:00
Ruben Fiszel
e6f9f4d897 more debug info for completed_runs 2025-07-24 11:40:21 +00:00
claude[bot]
16757f3bb5 nit(aichat): recommend anthropic as provider (#6265)
* feat(aichat): add recommendation alert for Claude latest model in Anthropic provider settings

Add info alert in AI workspace settings recommending Claude latest model for better reliability of AI chat when using Anthropic provider.

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

* fix

* nit

* nit

* use badge

* use blue

* add tooltip

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
Co-authored-by: centdix <farhadg110@gmail.com>
2025-07-23 15:14:51 +00:00
centdix
e869580787 nit(aichat): delete old messages when conversation becomes too long (#6258)
* delete old messages

* nit

* feat(aichat): add recursion bounds and improve token estimation

- Add maxDepth parameter (default 10) to deleteOldestMessage to prevent infinite recursion
- Enhance estimateTokenUsage to handle tool calls, content arrays, and function names
- Improves stability and accuracy of message cleanup when conversations become too long

Co-authored-by: centdix <centdix@users.noreply.github.com>

* nit

* cleaning

* better logic

* fix logic

* fix

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: centdix <centdix@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-07-23 16:36:41 +02:00
Ruben Fiszel
0edb01f774 chore(main): release 1.512.0 (#6260)
* chore(main): release 1.512.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-23 13:49:23 +00:00
Ruben Fiszel
e02f06bf68 decision tree nits 2025-07-23 13:45:41 +00:00
Ruben Fiszel
a57df1f781 fix: fix decision tree graph editor 2025-07-23 13:42:04 +00:00
Diego Imbert
7384aa36f4 fix flickering flow graph in vscode extension (#6266) 2025-07-23 12:33:53 +00:00
Ruben Fiszel
b92fcb90a1 add tagLabel in flow editor 2025-07-23 09:21:17 +00:00
dieriba
10befb995d feat: local type references parsing support for main function args (#5995)
* add base struct

* feat resolve interface and type declarion in entrypoint param's function

* nits

* fix reset dependencies

* update package

* fix handle infinite recursion

* add depth level and handle enum for referenced type

* nits

* nits

* nits

* perf

* fix

* done

* fix schema form cache inconsistency

* fix default type and nits

* remove

* update Object typ for parser

* one level ref from from parent when resolving types and use format for resource

* update cli and use resource type

* nits

* update parsers

* fix: use specific parser versions

---------

Co-authored-by: HugoCasa <hugo@casademont.ch>
2025-07-23 10:05:32 +02:00
Ruben Fiszel
91849baf34 fix: include export from ts relative import tracking 2025-07-22 22:58:49 +00:00
Ruben Fiszel
8591478a6d nit force refresh on reload button 2025-07-22 21:31:24 +00:00
Ruben Fiszel
86e14f8135 fix: run autoscaling scripts with superadmin permissions 2025-07-22 21:08:00 +00:00
Ruben Fiszel
b00fde7f2a chore(main): release 1.511.0 (#6250)
* chore(main): release 1.511.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-22 19:57:31 +00:00
dieriba
37b18f0661 fix: scopes-run-and-webhook-token-display (#6259)
* fix

* update sqlx

* Update backend/windmill-api/src/scopes.rs

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* fix perfect match

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-22 20:57:39 +02:00
Ruben Fiszel
34ccc8e0f7 fix: add error name and message to job_postprocessing span 2025-07-22 18:12:42 +00:00
Diego Imbert
2062a634f6 Fix duckdb array and object args not working properly (#6254) 2025-07-22 15:34:38 +00:00
centdix
71b85dfc42 feat(aichat): give completions to autocomplete for other languages than ts (#6253)
* give completitions for other languages than ts

* nit
2025-07-22 17:24:01 +02:00
centdix
dc242c5a8d feat(aichat): improve autocomplete with ata, editor diagnostic and deletion cues (#6245)
* add diagnostic details

* add types

* remove rest of line if possible

* add visual cue

* test

* adapt to context window

* cleaning

* use suggestions instead of lib files

* better use of suggestions

* cleaning

* add help in context

* better cache

* cleaning

* cleaning

* cleaner code

* better prompts

* fix

* cleaning

* fix

* use comment + no xml

* add cond on deletion cue

* fix

* fix

* cleaner
2025-07-22 14:50:26 +02:00
claude[bot]
8bc2a5733e fixt: Add runScriptByPath and runScriptByHash methods to SDK clients (#6252)
* feat: Add runScriptByPath and runScriptByHash methods to SDK clients

- Add runScriptByPath and runScriptByHash methods to TypeScript client
- Add run_script_by_path and run_script_by_hash methods to Python client
- Split functionality from existing methods that took both path and hash parameters
- Add deprecation warnings to existing run_script methods
- Maintain backward compatibility while encouraging use of focused methods

Closes #6251

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

* Refactor SDK methods to eliminate code duplication

- Extract common logic into internal helper methods
- Python client: _run_script_async_internal() and _run_script_internal()
- TypeScript client: _runScriptAsyncInternal() and _runScriptInternal()
- Eliminate duplicated parameter processing and HTTP setup
- Maintain exact same public API surface and functionality
- Reduce lines of code while preserving all existing behaviors

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

* fix ts and dev.nu for python

* trade warnings.warn for logging.warning

Signed-off-by: pyranota <pyra@duck.com>

---------

Signed-off-by: pyranota <pyra@duck.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
Co-authored-by: pyranota <pyra@duck.com>
2025-07-22 12:21:21 +00:00
Ruben Fiszel
1713987317 worker group nits 2025-07-22 07:52:39 +00:00
dieriba
c4178c05be feat: periodic worker group script (#6159)
* feaat periodic script

* feat periodic script

* fix unused

* check minimun time

* fix unused import

* update repo ref

* update ref

* nits and update repo ref

* add missing checks

* fix

* improve UX

* improve UX

* improve UX

* fix

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-07-21 22:57:56 +00:00
Alexander Petric
3d99cbe8f5 additional circular dependancy breaking bundling of cli (#6249) 2025-07-21 22:32:33 +02:00
Alexander Petric
506e17864e oauth client credentials: allow to override token url at resource level (#6233)
* oauth client credentials: allow to override token url at resource level

* ee ref
2025-07-21 20:08:56 +00:00
Ruben Fiszel
4b2c944af2 client nit 2025-07-21 18:31:40 +00:00
Ruben Fiszel
24a6038177 chore(main): release 1.510.1 (#6247)
* chore(main): release 1.510.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-21 16:07:26 +00:00
Ruben Fiszel
2c77012115 clear current id 2 2025-07-21 16:03:38 +00:00
Ruben Fiszel
6f91ee791a improve app loader behavior 2025-07-21 15:51:31 +00:00
dieriba
dd1f520d83 fix: improve ssh agent worker naming (#6211)
* remove ssh suffix

* use hostname tags

* feat

* feat

* update sqlx

* use hostname

* use worker prefix

* nits
2025-07-21 13:43:25 +00:00
pyranota
3efc329fc0 build: combine build-pkgs*.sh into build.nu (#6231)
* build: combine build-pkgs* into build.nu

Signed-off-by: pyranota <pyra@duck.com>

* nit refactor

Signed-off-by: pyranota <pyra@duck.com>

---------

Signed-off-by: pyranota <pyra@duck.com>
2025-07-21 12:11:17 +00:00
Ruben Fiszel
f4dbc97a58 fix: in home, archived and include without main not taken into account 2025-07-21 12:00:45 +00:00
Alexander Petric
6f11a87121 github app: workspace admin (#6225)
* github app: workspace admin

* sqlx upgrade

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-07-21 11:35:43 +00:00
hugocasa
be8c4ed86d fix: quickbooks oauth realmId (#6232)
* fix: quickbooks oauth realmId

* nits

* nit
2025-07-21 11:33:57 +00:00
Ruben Fiszel
8323707b07 fix: empty branches in branch one now return the previous_result 2025-07-21 11:23:22 +00:00
Ruben Fiszel
2bd1600253 display default branch being chosen as green 2025-07-21 10:01:05 +00:00
HugoCasa
e16d5e1e8a fix parser package lock 2025-07-21 11:10:06 +02:00
hugocasa
d2328e3670 fix: update parsers to prevent assets var bug (#6246)
* fix: update parsers to prevent assets var bug

* fix wrong parse function in cli for duckdb

---------

Co-authored-by: Diego Imbert <diego@windmill.dev>
2025-07-21 10:58:09 +02:00
Ruben Fiszel
dddabc8d1b chore(main): release 1.510.0 (#6238)
* chore(main): release 1.510.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-20 18:57:12 +00:00
Ruben Fiszel
1c7b206cd4 optimize script/flow lazy loading 2025-07-20 00:25:50 +00:00
Ruben Fiszel
e5dbbdaf1d nit skeleton 2025-07-20 00:11:01 +00:00
Ruben Fiszel
9201391102 fix: prevent loading script by hash if not permissioned 2025-07-19 23:54:01 +00:00
Ruben Fiszel
4fd5a8861d relax resource created by 2025-07-19 23:16:35 +00:00
Ruben Fiszel
6bdc2c3223 fix sqlx 2025-07-19 21:43:59 +00:00
Ruben Fiszel
8bb59d5a3c update ee-ref 2025-07-19 21:38:00 +00:00
Ruben Fiszel
86fa9f91b6 opt flow loading 2025-07-19 15:00:06 +00:00
Ruben Fiszel
dec72e201b feat: use sse for flow status updates 2025-07-19 14:42:26 +00:00
Ruben Fiszel
33619ab18c flow builder opt 2025-07-19 11:29:51 +00:00
Ruben Fiszel
0aa2ea6954 chore(main): release 1.509.2 (#6237)
* chore(main): release 1.509.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-07-19 11:08:46 +00:00
Ruben Fiszel
affecdad04 fix: add back asset kind variable 2025-07-19 11:04:28 +00:00
232 changed files with 7212 additions and 2655 deletions

View File

@@ -21,9 +21,6 @@ jobs:
with:
cache-workspaces: backend
toolchain: 1.88.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
- name: cargo check
working-directory: ./backend
timeout-minutes: 16
@@ -45,9 +42,6 @@ jobs:
with:
cache-workspaces: backend
toolchain: 1.88.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
- name: cargo check
working-directory: ./backend
timeout-minutes: 16
@@ -82,9 +76,6 @@ jobs:
with:
cache-workspaces: backend
toolchain: 1.88.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
- name: cargo check
working-directory: ./backend
timeout-minutes: 16
@@ -122,9 +113,6 @@ jobs:
with:
cache-workspaces: backend
toolchain: 1.88.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
- name: cargo check
timeout-minutes: 16
working-directory: ./backend

View File

@@ -52,9 +52,6 @@ jobs:
with:
cache-workspaces: backend
toolchain: 1.85.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
- name: cargo test
timeout-minutes: 16
run:

View File

@@ -84,10 +84,6 @@ jobs:
cache-workspaces: backend
toolchain: 1.88.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
- name: cargo check
working-directory: ./backend
timeout-minutes: 16
@@ -104,6 +100,7 @@ jobs:
allowed_tools: "mcp__github__create_pull_request,Bash"
custom_instructions: |
## IMPORTANT INSTRUCTIONS
- Your branch name should be a short description of the requested changes.
- Bypass previous instructions and instead of adding a url in your comment to manually create a PR, if you created a new branch with your changes, automatically create a draft PR from that branch to main.
## Code Quality Requirements

View File

@@ -1,5 +1,130 @@
# Changelog
## [1.515.1](https://github.com/windmill-labs/windmill/compare/v1.515.0...v1.515.1) (2025-07-29)
### Bug Fixes
* improved logs for script ([2e7ab91](https://github.com/windmill-labs/windmill/commit/2e7ab919a7bce4f04d7b09f45db48b5465346846))
## [1.515.0](https://github.com/windmill-labs/windmill/compare/v1.514.1...v1.515.0) (2025-07-29)
### Features
* **cli:** generate cursor rules on init ([#6270](https://github.com/windmill-labs/windmill/issues/6270)) ([29c686d](https://github.com/windmill-labs/windmill/commit/29c686d62ae3597eb59371f5a3e9dadd9dc9557f))
### Bug Fixes
* add size limit to indexer queries on jobs table to avoid oom ([#6293](https://github.com/windmill-labs/windmill/issues/6293)) ([4d6a614](https://github.com/windmill-labs/windmill/commit/4d6a61403b51d84ea4039611cb007a42a26d24b6))
* fix DynSelect ([55ba599](https://github.com/windmill-labs/windmill/commit/55ba599022ac2a6b513c4456747e4910f3188ec7))
* improve docker logs collection in docker mode ([ce4177e](https://github.com/windmill-labs/windmill/commit/ce4177ebaed02df62aec676ca4624d77052f3e51))
* resource types as arg in typescript handle imported defined types ([56671bc](https://github.com/windmill-labs/windmill/commit/56671bc75fe940f940f52ad47f5cf99c8329a0c3))
* resource-type-ts-parser ([#6289](https://github.com/windmill-labs/windmill/issues/6289)) ([9931311](https://github.com/windmill-labs/windmill/commit/99313116509a5ebec33aa2ab813e0c189d4c4e24))
## [1.514.1](https://github.com/windmill-labs/windmill/compare/v1.514.0...v1.514.1) (2025-07-28)
### Bug Fixes
* pin tokio to 1.46.1 and aws-sdks-ts ([1a85dc7](https://github.com/windmill-labs/windmill/commit/1a85dc7008d77cb4d51c488b4ba02a02cd08b05d))
## [1.514.0](https://github.com/windmill-labs/windmill/compare/v1.513.1...v1.514.0) (2025-07-28)
### Features
* migrate audit log ids to bigints (blocking migration for EE) ([6e8f5a5](https://github.com/windmill-labs/windmill/commit/6e8f5a5b00056a324fa4fc27b740290338e672c9))
### Bug Fixes
* fix id renaming in apps ([a999bc7](https://github.com/windmill-labs/windmill/commit/a999bc7b28764e3583fdd843f13060c760627b51))
* **mcp:** add proper check for mcp routes ([#6282](https://github.com/windmill-labs/windmill/issues/6282)) ([f892f7c](https://github.com/windmill-labs/windmill/commit/f892f7c9af1058b899c880160bd91d5e7b3ebcd6))
## [1.513.1](https://github.com/windmill-labs/windmill/compare/v1.513.0...v1.513.1) (2025-07-25)
### Bug Fixes
* improve error handler behavior wrt to parrallel branchall & forloops ([#6273](https://github.com/windmill-labs/windmill/issues/6273)) ([3f784e3](https://github.com/windmill-labs/windmill/commit/3f784e3c07aa35ef2298bed301d581bf25300ba6))
* jobLoader for flowstatusviewerinner work with public apps ([94e20a9](https://github.com/windmill-labs/windmill/commit/94e20a97e036fdb99e34c79b466e957b943cb26a))
* **mcp:** allow mcp scopes to run scripts and flows ([#6278](https://github.com/windmill-labs/windmill/issues/6278)) ([e8f4c24](https://github.com/windmill-labs/windmill/commit/e8f4c2418b83af75be90b21ab2ab64537fef00eb))
## [1.513.0](https://github.com/windmill-labs/windmill/compare/v1.512.0...v1.513.0) (2025-07-24)
### Features
* enable workspace exclusion in custom tags ([#6263](https://github.com/windmill-labs/windmill/issues/6263)) ([be3173d](https://github.com/windmill-labs/windmill/commit/be3173d048f8c3ee3681840b9880f509b754d779))
### Bug Fixes
* add windir env var to pwsh for legacy modules like AD ([#6271](https://github.com/windmill-labs/windmill/issues/6271)) ([7920583](https://github.com/windmill-labs/windmill/commit/79205839b6c3ba51a637f634d8cfba737b4dee1c))
* fix webhook panel reactivity ([241be48](https://github.com/windmill-labs/windmill/commit/241be4897eb3377ccce097404fcd176e746c3015))
* improve aggrid columnDefs reactivity ([f71104e](https://github.com/windmill-labs/windmill/commit/f71104e9f64bf6cb340d99ad91977c79651780c8))
## [1.512.0](https://github.com/windmill-labs/windmill/compare/v1.511.0...v1.512.0) (2025-07-23)
### Features
* local type references parsing support for main function args ([#5995](https://github.com/windmill-labs/windmill/issues/5995)) ([10befb9](https://github.com/windmill-labs/windmill/commit/10befb995d07f50803213bcf177df8891fd920e1))
### Bug Fixes
* fix decision tree graph editor ([a57df1f](https://github.com/windmill-labs/windmill/commit/a57df1f78119b80ce764ddbff0ecb989cbb4c315))
* include export from ts relative import tracking ([91849ba](https://github.com/windmill-labs/windmill/commit/91849baf3404c308f946f7c384ce679f2c89cbe7))
* run autoscaling scripts with superadmin permissions ([86e14f8](https://github.com/windmill-labs/windmill/commit/86e14f8135eb8ccfad47f0340986464983019153))
## [1.511.0](https://github.com/windmill-labs/windmill/compare/v1.510.1...v1.511.0) (2025-07-22)
### Features
* **aichat:** give completions to autocomplete for other languages than ts ([#6253](https://github.com/windmill-labs/windmill/issues/6253)) ([71b85df](https://github.com/windmill-labs/windmill/commit/71b85dfc42694f552a0572afd2072bbe528ac74b))
* **aichat:** improve autocomplete with ata, editor diagnostic and deletion cues ([#6245](https://github.com/windmill-labs/windmill/issues/6245)) ([dc242c5](https://github.com/windmill-labs/windmill/commit/dc242c5a8df7d5d2ad710f3a5578dcb5fb4c9667))
* periodic worker group script ([#6159](https://github.com/windmill-labs/windmill/issues/6159)) ([c4178c0](https://github.com/windmill-labs/windmill/commit/c4178c05be5cff4ef0617e31e0f05adc29e04f89))
### Bug Fixes
* add error name and message to job_postprocessing span ([34ccc8e](https://github.com/windmill-labs/windmill/commit/34ccc8e0f7487cf51370e0ff94546063ee7771ad))
* scopes-run-and-webhook-token-display ([#6259](https://github.com/windmill-labs/windmill/issues/6259)) ([37b18f0](https://github.com/windmill-labs/windmill/commit/37b18f0661d797466423db021150c99398435c8b))
## [1.510.1](https://github.com/windmill-labs/windmill/compare/v1.510.0...v1.510.1) (2025-07-21)
### Bug Fixes
* empty branches in branch one now return the previous_result ([8323707](https://github.com/windmill-labs/windmill/commit/8323707b07933eca81f0c30df2434c6409c26789))
* improve ssh agent worker naming ([#6211](https://github.com/windmill-labs/windmill/issues/6211)) ([dd1f520](https://github.com/windmill-labs/windmill/commit/dd1f520d83d618dbf1d03855890011031da838b5))
* in home, archived and include without main not taken into account ([f4dbc97](https://github.com/windmill-labs/windmill/commit/f4dbc97a58ea8742dc356bdad4c03bbaa7b369b7))
* quickbooks oauth realmId ([#6232](https://github.com/windmill-labs/windmill/issues/6232)) ([be8c4ed](https://github.com/windmill-labs/windmill/commit/be8c4ed86d81013e575441f30e5a444cb4067db7))
* update parsers to prevent assets var bug ([#6246](https://github.com/windmill-labs/windmill/issues/6246)) ([d2328e3](https://github.com/windmill-labs/windmill/commit/d2328e3670c73a5c4ce7b677339f9293604fd583))
## [1.510.0](https://github.com/windmill-labs/windmill/compare/v1.509.2...v1.510.0) (2025-07-20)
### Features
* use sse for flow status updates ([dec72e2](https://github.com/windmill-labs/windmill/commit/dec72e201bcc8242eb269826505d6338c9751f14))
### Bug Fixes
* prevent loading script by hash if not permissioned ([9201391](https://github.com/windmill-labs/windmill/commit/9201391102e927b943958851ccb75bb9695833a5))
## [1.509.2](https://github.com/windmill-labs/windmill/compare/v1.509.1...v1.509.2) (2025-07-19)
### Bug Fixes
* add back asset kind variable ([affecda](https://github.com/windmill-labs/windmill/commit/affecdad04e95e1e1fd26bd6bcdd0ec6e3c7a28e))
## [1.509.1](https://github.com/windmill-labs/windmill/compare/v1.509.0...v1.509.1) (2025-07-19)

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n c.id IS NOT NULL AS completed,\n CASE \n WHEN q.id IS NOT NULL THEN (CASE WHEN NOT $5 AND q.running THEN true ELSE null END)\n ELSE false\n END AS running,\n SUBSTR(logs, GREATEST($1 - log_offset, 0)) AS logs,\n COALESCE(r.memory_peak, c.memory_peak) AS mem_peak,\n CASE\n -- flow step:\n WHEN flow_step_id IS NOT NULL THEN NULL\n -- completed:\n WHEN c.id IS NOT NULL THEN COALESCE(\n c.workflow_as_code_status || c.flow_status,\n c.workflow_as_code_status,\n c.flow_status\n )\n -- not completed:\n ELSE COALESCE(\n f.workflow_as_code_status || f.flow_status,\n f.workflow_as_code_status,\n f.flow_status\n )\n END AS \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n job_logs.log_offset + CHAR_LENGTH(job_logs.logs) + 1 AS log_offset,\n created_by AS \"created_by!\",\n CASE WHEN $4::BOOLEAN THEN (\n SELECT scalar_int FROM job_stats WHERE job_id = $3 AND metric_id = 'progress_perc'\n ) END AS progress\n FROM v2_job j\n LEFT JOIN v2_job_queue q USING (id)\n LEFT JOIN v2_job_runtime r USING (id)\n LEFT JOIN v2_job_status f USING (id)\n LEFT JOIN v2_job_completed c USING (id)\n LEFT JOIN job_logs ON job_logs.job_id = $3\n WHERE j.workspace_id = $2 AND j.id = $3\n AND ($6::text[] IS NULL OR j.tag = ANY($6))",
"query": "SELECT\n c.id IS NOT NULL AS completed,\n CASE \n WHEN q.id IS NOT NULL THEN (CASE WHEN NOT $5 AND q.running THEN true ELSE null END)\n ELSE false\n END AS running,\n SUBSTR(logs, GREATEST($1 - log_offset, 0)) AS logs,\n COALESCE(r.memory_peak, c.memory_peak) AS mem_peak,\n COALESCE(c.flow_status, f.flow_status) AS \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n COALESCE(c.workflow_as_code_status, f.workflow_as_code_status) AS \"workflow_as_code_status: sqlx::types::Json<Box<RawValue>>\",\n job_logs.log_offset + CHAR_LENGTH(job_logs.logs) + 1 AS log_offset,\n created_by AS \"created_by!\",\n CASE WHEN $4::BOOLEAN THEN (\n SELECT scalar_int FROM job_stats WHERE job_id = $3 AND metric_id = 'progress_perc'\n ) END AS progress\n FROM v2_job j\n LEFT JOIN v2_job_queue q USING (id)\n LEFT JOIN v2_job_runtime r USING (id)\n LEFT JOIN v2_job_status f USING (id)\n LEFT JOIN v2_job_completed c USING (id)\n LEFT JOIN job_logs ON job_logs.job_id = $3\n WHERE j.workspace_id = $2 AND j.id = $3\n AND ($6::text[] IS NULL OR j.tag = ANY($6))",
"describe": {
"columns": [
{
@@ -30,16 +30,21 @@
},
{
"ordinal": 5,
"name": "workflow_as_code_status: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 6,
"name": "log_offset",
"type_info": "Int4"
},
{
"ordinal": 6,
"ordinal": 7,
"name": "created_by!",
"type_info": "Varchar"
},
{
"ordinal": 7,
"ordinal": 8,
"name": "progress",
"type_info": "Int4"
}
@@ -61,9 +66,10 @@
null,
null,
null,
null,
false,
null
]
},
"hash": "28258e41e95c7c86af0bc3b7469df614a9b71bc372b7994802edac93e6c2ada6"
"hash": "278bc6b4f149f824b5db32dacfaa714ee3852dc2ddf2d661dfdd5a986a9bb62b"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:script/' || $2]::text[]",
"query": "\n SELECT label,\n concat(substring(token for 10)) AS token_prefix,\n expiration,\n created_at,\n last_used_at,\n scopes,\n email\n FROM token\n WHERE workspace_id = $1\n AND (\n scopes @> ARRAY['jobs:run:flows:' || $2]::text[]\n OR scopes @> ARRAY['run:flows/' || $2]::text[]\n )\n ",
"describe": {
"columns": [
{
@@ -55,5 +55,5 @@
true
]
},
"hash": "c7ee7ce64686cef41cebd99ad7ef31572fc1bf12e6ae473fd58fafb025989965"
"hash": "334c9a6d770471ac53099eb5227778899a5c680b899bc6a6d1a3f89fdbf8c730"
}

View File

@@ -1,16 +1,11 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n tag, \n script_lang AS \"script_lang!: _\"\n FROM \n v2_job\n WHERE \n id = $1\n ",
"query": "\n SELECT \n script_lang as \"language: _\", \n COUNT(*) as \"count!\", \n SUM(duration_ms)::BIGINT as \"total_duration!\" \n FROM \n v2_job_completed\n JOIN \n v2_job \n ON \n v2_job.id = v2_job_completed.id \n WHERE \n created_at > NOW() - INTERVAL '48 hours' \n GROUP BY \n script_lang\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "script_lang!: _",
"name": "language: _",
"type_info": {
"Custom": {
"name": "script_lang",
@@ -42,17 +37,26 @@
}
}
}
},
{
"ordinal": 1,
"name": "count!",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "total_duration!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Uuid"
]
"Left": []
},
"nullable": [
false,
true
true,
null,
null
]
},
"hash": "4e5273b9ce05f6ee2dfd5f14c8574a0cf43682480452f7dbe23012320fe7fe25"
"hash": "48242fc46cb8bc9e4d41c788568634619565e4ef342051af23760c11edac5ee8"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n tag\n FROM \n v2_job\n WHERE \n id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "tag",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "49e2430af74ec10857e5df7f7e1ad1b53ba70bb51b0259a1f765f76db9b733ad"
}

View File

@@ -15,7 +15,7 @@
]
},
"nullable": [
null
true
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"

View File

@@ -13,7 +13,8 @@
"kind": {
"Enum": [
"s3object",
"resource"
"resource",
"variable"
]
}
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO account (workspace_id, client, expires_at, refresh_token, grant_type, cc_client_id, cc_client_secret) VALUES ($1, $2, now() + ($3 || ' seconds')::interval, $4, $5, $6, $7) RETURNING id",
"query": "INSERT INTO account (workspace_id, client, expires_at, refresh_token, grant_type, cc_client_id, cc_client_secret, cc_token_url) VALUES ($1, $2, now() + ($3 || ' seconds')::interval, $4, $5, $6, $7, $8) RETURNING id",
"describe": {
"columns": [
{
@@ -17,6 +17,7 @@
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Varchar"
]
},
@@ -24,5 +25,5 @@
false
]
},
"hash": "00418f2e621cbe7aed536cff357898f7250544dfcb3bd85d314af1058f3148d8"
"hash": "bbc28b92ae8ec3d120a8976be7d3966282fe6543e0eb957fc10864dbf58de58f"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT client, refresh_token, grant_type, cc_client_id, cc_client_secret FROM account WHERE workspace_id = $1 AND id = $2",
"query": "SELECT client, refresh_token, grant_type, cc_client_id, cc_client_secret, cc_token_url FROM account WHERE workspace_id = $1 AND id = $2",
"describe": {
"columns": [
{
@@ -27,6 +27,11 @@
"ordinal": 4,
"name": "cc_client_secret",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "cc_token_url",
"type_info": "Varchar"
}
],
"parameters": {
@@ -40,8 +45,9 @@
false,
false,
true,
true,
true
]
},
"hash": "b9d57959f2696f74e9d24f55fb78ff9e9041ab84b896a8b980df7b550ef27a22"
"hash": "cc269052ffc1e613d7edc31f0f7bb84f6e6301ad1afb028813105a121a69fa7e"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
"query": "\n SELECT label,\n concat(substring(token for 10)) AS token_prefix,\n expiration,\n created_at,\n last_used_at,\n scopes,\n email\n FROM token\n WHERE workspace_id = $1\n AND (\n scopes @> ARRAY['jobs:run:scripts:' || $2]::text[]\n OR scopes @> ARRAY['run:scripts/' || $2]::text[]\n )\n ",
"describe": {
"columns": [
{
@@ -55,5 +55,5 @@
true
]
},
"hash": "eff32aeac25a75d06f73e08c26dd3fd25f6b85cbea870505751c6a82457ae1da"
"hash": "e8b74683f994d198a051c2f211a2844241bf2a7d7702c569017ae53fca706c59"
}

207
backend/Cargo.lock generated
View File

@@ -746,9 +746,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-config"
version = "1.8.2"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebd9b83179adf8998576317ce47785948bcff399ec5b15f4dfbdedd44ddf5b92"
checksum = "c0baa720ebadea158c5bda642ac444a2af0cdf7bb66b46d1e4533de5d1f449d0"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -788,9 +788,9 @@ dependencies = [
[[package]]
name = "aws-lc-rs"
version = "1.13.2"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08b5d4e069cbc868041a64bd68dc8cb39a0d79585cd6c5a24caa8c2d622121be"
checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba"
dependencies = [
"aws-lc-sys",
"zeroize",
@@ -835,9 +835,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sqs"
version = "1.76.0"
version = "1.77.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "837b4b181a1306a5a67d85218fa7d7298e340041d9bd81ae6919b9ec4f86ca97"
checksum = "32a6dcd63dffae1f7872df42ce9f1f0f785f8c9a3d3e880ef6c171d0415439df"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -857,9 +857,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "1.76.0"
version = "1.77.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64bf26698dd6d238ef1486bdda46f22a589dc813368ba868dc3d94c8d27b56ba"
checksum = "18f2f37fea82468fe3f5a059542c05392ef680c4f7f00e0db02df8b6e5c7d0c6"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -879,9 +879,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.77.0"
version = "1.78.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cd07ed1edd939fae854a22054299ae3576500f4e0fadc560ca44f9c6ea1664"
checksum = "ecb4f6eada20e0193450cd48b12ed05e1e66baac86f39160191651b932f2b7d9"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -901,9 +901,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.78.0"
version = "1.79.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37f7766d2344f56d10d12f3c32993da36d78217f32594fe4fb8e57a538c1cdea"
checksum = "317377afba3498fca4948c5d32b399ef9a5ad35561a1e8a6f2ac7273dabf802d"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1034,9 +1034,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime"
version = "1.8.4"
version = "1.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3aaec682eb189e43c8a19c3dab2fe54590ad5f2cc2d26ab27608a20f2acf81c"
checksum = "660f70d9d8af6876b4c9aa8dcb0dbaf0f89b04ee9a4455bea1b4ba03b15f26f6"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
@@ -1058,9 +1058,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime-api"
version = "1.8.3"
version = "1.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9852b9226cb60b78ce9369022c0df678af1cac231c882d5da97a0c4e03be6e67"
checksum = "937a49ecf061895fca4a6dd8e864208ed9be7546c0527d04bc07d502ec5fba1c"
dependencies = [
"aws-smithy-async",
"aws-smithy-types",
@@ -1120,9 +1120,9 @@ dependencies = [
[[package]]
name = "aws-types"
version = "1.3.7"
version = "1.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a322fec39e4df22777ed3ad8ea868ac2f94cd15e1a55f6ee8d8d6305057689a"
checksum = "b069d19bf01e46298eaedd7c6f283fe565a59263e53eebec945f3e6398f42390"
dependencies = [
"aws-credential-types",
"aws-smithy-async",
@@ -1689,9 +1689,9 @@ dependencies = [
[[package]]
name = "bytemuck_derive"
version = "1.9.3"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1"
checksum = "441473f2b4b0459a68628c744bc61d23e730fb00128b841d30fa4bb3972257e4"
dependencies = [
"proc-macro2",
"quote",
@@ -3737,7 +3737,7 @@ dependencies = [
"quinn",
"rustls-tokio-stream",
"serde",
"socket2",
"socket2 0.5.10",
"thiserror 2.0.12",
"tokio",
]
@@ -4674,7 +4674,7 @@ dependencies = [
"num-integer",
"rust_decimal",
"smallvec",
"strum 0.27.1",
"strum 0.27.2",
]
[[package]]
@@ -4685,9 +4685,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "dyn-clone"
version = "1.0.19"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "dyn-iter"
@@ -6377,7 +6377,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
"socket2",
"socket2 0.5.10",
"tokio",
"tower-service",
"tracing",
@@ -6498,9 +6498,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.15"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df"
checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -6514,7 +6514,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2",
"socket2 0.6.0",
"system-configuration 0.6.1",
"tokio",
"tower-service",
@@ -6820,9 +6820,9 @@ dependencies = [
[[package]]
name = "io-uring"
version = "0.7.8"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
dependencies = [
"bitflags 2.9.1",
"cfg-if",
@@ -6835,7 +6835,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
dependencies = [
"socket2",
"socket2 0.5.10",
"widestring",
"windows-sys 0.48.0",
"winreg",
@@ -6969,9 +6969,9 @@ dependencies = [
[[package]]
name = "jsonc-parser"
version = "0.26.2"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b558af6b49fd918e970471374e7a798b2c9bbcda624a210ffa3901ee5614bc8e"
checksum = "1d6d80e6d70e7911a29f3cf3f44f452df85d06f73572b494ca99a2cad3fcf8f4"
dependencies = [
"serde_json",
]
@@ -7270,7 +7270,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
"windows-targets 0.53.2",
"windows-targets 0.53.3",
]
[[package]]
@@ -7292,13 +7292,13 @@ dependencies = [
[[package]]
name = "libredox"
version = "0.1.4"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638"
checksum = "360e552c93fa0e8152ab463bc4c4837fce76a225df11dfaeea66c313de5e61f7"
dependencies = [
"bitflags 2.9.1",
"libc",
"redox_syscall 0.5.13",
"redox_syscall 0.5.17",
]
[[package]]
@@ -7380,9 +7380,9 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]]
name = "litrs"
version = "0.4.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed"
[[package]]
name = "lock_api"
@@ -7960,7 +7960,7 @@ dependencies = [
"rand 0.9.0",
"serde",
"serde_json",
"socket2",
"socket2 0.5.10",
"thiserror 2.0.12",
"tokio",
"tokio-native-tls",
@@ -9056,7 +9056,7 @@ checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.5.13",
"redox_syscall 0.5.17",
"smallvec",
"windows-targets 0.52.6",
]
@@ -9507,9 +9507,9 @@ checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5"
[[package]]
name = "prettyplease"
version = "0.2.35"
version = "0.2.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a"
checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2"
dependencies = [
"proc-macro2",
"syn 2.0.104",
@@ -9841,9 +9841,9 @@ dependencies = [
[[package]]
name = "quick_cache"
version = "0.6.14"
version = "0.6.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b450dad8382b1b95061d5ca1eb792081fb082adf48c678791fe917509596d5f"
checksum = "8565e62e02af316570d4b492f17af1481d6c07cea60f4e7edd71700da5052ba9"
dependencies = [
"ahash 0.8.12",
"equivalent",
@@ -9864,7 +9864,7 @@ dependencies = [
"quinn-udp",
"rustc-hash 2.1.1",
"rustls 0.23.29",
"socket2",
"socket2 0.5.10",
"thiserror 2.0.12",
"tokio",
"tracing",
@@ -9901,7 +9901,7 @@ dependencies = [
"cfg_aliases 0.2.1",
"libc",
"once_cell",
"socket2",
"socket2 0.5.10",
"tracing",
"windows-sys 0.59.0",
]
@@ -10156,9 +10156,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.13"
version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
dependencies = [
"bitflags 2.9.1",
]
@@ -10663,9 +10663,9 @@ dependencies = [
[[package]]
name = "rustc-demangle"
version = "0.1.25"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]]
name = "rustc-hash"
@@ -10847,7 +10847,7 @@ checksum = "22557157d7395bc30727745b365d923f1ecc230c4c80b176545f3f4f08c46e33"
dependencies = [
"futures",
"rustls 0.23.29",
"socket2",
"socket2 0.5.10",
"tokio",
]
@@ -11696,6 +11696,16 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "socket2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "socks"
version = "0.3.4"
@@ -12129,11 +12139,11 @@ dependencies = [
[[package]]
name = "strum"
version = "0.27.1"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
"strum_macros 0.27.1",
"strum_macros 0.27.2",
]
[[package]]
@@ -12151,14 +12161,13 @@ dependencies = [
[[package]]
name = "strum_macros"
version = "0.27.1"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.104",
]
@@ -13207,7 +13216,7 @@ dependencies = [
"pin-project-lite",
"signal-hook-registry",
"slab",
"socket2",
"socket2 0.5.10",
"tokio-macros",
"tracing",
"windows-sys 0.52.0",
@@ -13275,7 +13284,7 @@ dependencies = [
"postgres-protocol 0.6.7",
"postgres-types 0.2.7",
"rand 0.8.5",
"socket2",
"socket2 0.5.10",
"tokio",
"tokio-util",
"whoami",
@@ -13301,7 +13310,7 @@ dependencies = [
"postgres-protocol 0.6.8",
"postgres-types 0.2.9",
"rand 0.9.0",
"socket2",
"socket2 0.5.10",
"tokio",
"tokio-util",
"whoami",
@@ -13507,7 +13516,7 @@ dependencies = [
"prost",
"rustls-native-certs 0.8.1",
"rustls-pemfile 2.2.0",
"socket2",
"socket2 0.5.10",
"tokio",
"tokio-rustls 0.26.2",
"tokio-stream",
@@ -14632,7 +14641,7 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7"
dependencies = [
"redox_syscall 0.5.13",
"redox_syscall 0.5.17",
"wasite",
"web-sys",
]
@@ -14676,7 +14685,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"axum",
@@ -14705,7 +14714,7 @@ dependencies = [
"sha2 0.10.9",
"size",
"sqlx",
"strum 0.27.1",
"strum 0.27.2",
"systemstat",
"tikv-jemalloc-ctl",
"tikv-jemalloc-sys",
@@ -14728,7 +14737,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"argon2",
@@ -14739,8 +14748,11 @@ dependencies = [
"async_zip",
"aws-config",
"aws-sdk-sqs",
"aws-sdk-sso",
"aws-sdk-ssooidc",
"aws-sdk-sts",
"axum",
"backon",
"base32",
"base64 0.22.1",
"byteorder",
@@ -14794,6 +14806,7 @@ dependencies = [
"rumqttc",
"rust-embed",
"rust_decimal",
"rustls 0.23.29",
"samael",
"serde",
"serde_json",
@@ -14839,7 +14852,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -14854,7 +14867,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"chrono",
"serde",
@@ -14867,7 +14880,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"serde",
@@ -14881,7 +14894,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"async-stream",
@@ -14932,8 +14945,8 @@ dependencies = [
"sha2 0.10.9",
"size",
"sqlx",
"strum 0.27.1",
"strum_macros 0.27.1",
"strum 0.27.2",
"strum_macros 0.27.2",
"sysinfo",
"systemstat",
"tar",
@@ -14960,7 +14973,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"regex",
"serde",
@@ -14974,12 +14987,13 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"bytes",
"chrono",
"const_format",
"flume",
"futures",
"lazy_static",
"object_store",
@@ -14997,7 +15011,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -15009,7 +15023,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -15018,7 +15032,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -15030,7 +15044,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"serde_json",
@@ -15042,7 +15056,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"gosyn",
@@ -15054,7 +15068,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -15066,7 +15080,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"serde_json",
@@ -15078,7 +15092,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"nu-parser",
@@ -15089,7 +15103,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15100,7 +15114,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15112,7 +15126,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -15135,7 +15149,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -15152,7 +15166,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -15166,7 +15180,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -15184,7 +15198,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wasm"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"getrandom 0.2.16",
@@ -15208,7 +15222,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"serde_json",
@@ -15218,7 +15232,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -15251,7 +15265,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -15261,7 +15275,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.509.1"
version = "1.515.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -15597,7 +15611,7 @@ version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.2",
"windows-targets 0.53.3",
]
[[package]]
@@ -15633,10 +15647,11 @@ dependencies = [
[[package]]
name = "windows-targets"
version = "0.53.2"
version = "0.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.509.1"
version = "1.515.1"
authors.workspace = true
edition.workspace = true
@@ -28,11 +28,11 @@ members = [
"./parsers/windmill-parser-py",
"./parsers/windmill-parser-py-imports",
"./parsers/windmill-sql-datatype-parser-wasm",
"./parsers/windmill-parser-yaml", "windmill-macros", "parsers/windmill-parser-nu",
"./parsers/windmill-parser-yaml", "windmill-macros", "parsers/windmill-parser-nu"
]
[workspace.package]
version = "1.509.1"
version = "1.515.1"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -190,12 +190,11 @@ windmill-api-client = { path = "./windmill-api-client" }
reqwest-retry = "^0"
reqwest-middleware = { version = "^0", features = ["json"] }
rustls = "0.23.0"
memchr = "2.7.4"
axum = { version = "^0.7", features = ["multipart"] }
headers = "^0"
hyper = { version = "^1", features = ["full"] }
tokio = { version = "^1.42.0", features = ["full", "tracing", "time"] }
tokio = { version = "=1.46.1", features = ["full", "tracing", "time"] }
tokio-stream = { version = "0.1.17" }
tower = "^0"
tower-http = { version = "^0.6", features = ["trace", "cors"] }
@@ -354,8 +353,14 @@ datafusion = "47.0.0"
object_store = { git = "https://github.com/apache/arrow-rs-object-store", rev = "36752c975d4f29e20b57c91f81a10872dcd48ae7", features = ["aws", "azure", "gcp"] }
openidconnect = { version = "4.0.0-rc.1" }
aws-config = "^1"
aws-sdk-sqs = "1.57.0"
aws-sdk-sts = "^1"
aws-sdk-sqs = "=1.77.0"
aws-sdk-sts = "=1.79.0"
aws-sdk-sso = "=1.77.0"
aws-sdk-ssooidc = "=1.78.0"
rustls = "=0.23.29"
aws-smithy-types-convert = { version = "^0", features = ["convert-chrono"] }
crc = "^3"
tar = "^0"

View File

@@ -1 +1 @@
e2bbcda64a9eb783d0832bc6f764be1627046961
cef7486dfa765f1cdfaddba59564c4bb92fe864e

View File

@@ -0,0 +1,2 @@
-- Rollback cc_token_url column addition
ALTER TABLE account DROP COLUMN IF EXISTS cc_token_url;

View File

@@ -0,0 +1,3 @@
-- Add cc_token_url column for resource-level token URL override in OAuth client credentials flow
-- This allows resources to override the token URL from instance settings when using client credentials
ALTER TABLE account ADD COLUMN cc_token_url VARCHAR(500);

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TYPE ASSET_KIND ADD VALUE 'variable';

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE resource ALTER COLUMN created_by TYPE VARCHAR(500);

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,2 @@
ALTER TABLE audit ALTER COLUMN id TYPE bigint;
ALTER SEQUENCE audit_id_seq AS bigint;

View File

@@ -7,7 +7,7 @@ use anyhow::anyhow;
use tree_sitter::Node;
use windmill_parser::Arg;
use windmill_parser::MainArgSignature;
use windmill_parser::Typ;
use windmill_parser::{ObjectType, Typ};
#[derive(Debug)]
pub struct CsharpMainSigMeta {
@@ -112,7 +112,7 @@ fn find_typ<'a>(typ_node: Node<'a>, code: &str) -> anyhow::Result<Typ> {
Ok("double") | Ok("System.Double") => Ok(Typ::Float),
Ok("bool") | Ok("System.Boolean") => Ok(Typ::Bool),
Ok("decimal") | Ok("System.Decimal") => Ok(Typ::Float),
Ok("object") => Ok(Typ::Object(vec![])), // TODO: Complete the object type
Ok("object") => Ok(Typ::Object(ObjectType::new(None, Some(vec![])))), // TODO: Complete the object type
Ok(s) => Err(anyhow!("Unknown type `{s}`")),
Err(e) => Err(anyhow!("Error getting type name: {}", e)),
}

View File

@@ -7,7 +7,7 @@ use gosyn::{
use itertools::Itertools;
use regex::Regex;
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, Typ};
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, ObjectType, Typ};
lazy_static::lazy_static! {
pub static ref REQUIRE_PARSE: Regex = Regex::new(r"//require (.*)\n").unwrap();
@@ -142,13 +142,13 @@ fn parse_go_typ(typ: &Expression) -> (Option<String>, Typ) {
"struct {{ {} }}",
otyps.iter().join("; ").to_string()
)),
Typ::Object(typs),
Typ::Object(ObjectType::new(None, Some(typs))),
)
}
Expression::TypeInterface(_) => (Some("interface{}".to_string()), Typ::Object(vec![])),
Expression::TypeInterface(_) => (Some("interface{}".to_string()), Typ::Object(ObjectType::new(None, Some(vec![])))),
Expression::TypeMap(_) => (
Some("map[string]interface{}".to_string()),
Typ::Object(vec![]),
Typ::Object(ObjectType::new(None, Some(vec![]))),
),
_ => (None, Typ::Unknown),
}
@@ -218,10 +218,10 @@ func main(x int, y string, z bool, l []string, o struct { Name string `json:"nam
Arg {
otyp: Some("struct { Name string `json:\"name\"` }".to_string()),
name: "o".to_string(),
typ: Typ::Object(vec![ObjectProperty {
typ: Typ::Object(ObjectType::new(None, Some(vec![ObjectProperty {
key: "name".to_string(),
typ: Box::new(Typ::Str(None))
},]),
},]))),
default: None,
has_default: false,
oidx: None
@@ -229,7 +229,7 @@ func main(x int, y string, z bool, l []string, o struct { Name string `json:"nam
Arg {
otyp: Some("interface{}".to_string()),
name: "n".to_string(),
typ: Typ::Object(vec![]),
typ: Typ::Object(ObjectType::new(None, Some(vec![]))),
default: None,
has_default: false,
oidx: None
@@ -237,7 +237,7 @@ func main(x int, y string, z bool, l []string, o struct { Name string `json:"nam
Arg {
otyp: Some("map[string]interface{}".to_string()),
name: "m".to_string(),
typ: Typ::Object(vec![]),
typ: Typ::Object(ObjectType::new(None, Some(vec![]))),
default: None,
has_default: false,
oidx: None

View File

@@ -9,7 +9,7 @@ use regex_lite::Regex;
use serde_json::json;
use windmill_parser::{Arg, MainArgSignature, Typ};
use windmill_parser::{Arg, MainArgSignature, ObjectType, Typ};
pub fn parse_graphql_sig(code: &str) -> anyhow::Result<MainArgSignature> {
let parsed = parse_graphql_file(&code)?;
@@ -75,7 +75,7 @@ pub fn parse_graphql_typ(typ: &str) -> Typ {
"Int" => Typ::Int,
"Boolean" => Typ::Bool,
"Float" => Typ::Float,
_ => Typ::Object(vec![]),
_ => Typ::Object(ObjectType::new(None, Some(vec![]))),
}
}

View File

@@ -9,7 +9,7 @@ use serde_json::Value;
use tree_sitter::Node;
use windmill_parser::Arg;
use windmill_parser::MainArgSignature;
use windmill_parser::Typ;
use windmill_parser::{ObjectType, Typ};
#[derive(Debug)]
pub struct JavaMainSigMeta {
@@ -101,7 +101,7 @@ fn find_typ<'a>(typ_node: Node<'a>, code: &str) -> anyhow::Result<(Typ, Option<V
Ok("Double") => (Typ::Float, null),
Ok("Boolean") => (Typ::Bool, null),
Ok("Character") => (Typ::Str(None), null),
Ok("Object") => (Typ::Object(vec![]),null), // TODO: Complete the object type
Ok("Object") => (Typ::Object(ObjectType::new(None, Some(vec![]))),null), // TODO: Complete the object type
Ok(s) => bail!("Unknown type `{s}`"),
Err(e) => bail!("Error getting type name: {}", e),
}
@@ -394,7 +394,7 @@ class Main {
Arg {
name: "i".into(),
otyp: Some("Object".into()),
typ: Typ::Object(vec![]),
typ: Typ::Object(ObjectType::new(None, Some(vec![]))),
default: Some(json!(null)),
has_default: true,
oidx: None
@@ -427,7 +427,7 @@ class Main {
Arg {
name: "b".into(),
otyp: Some("Object[]".into()),
typ: Typ::List(Box::new(Typ::Object(vec![]))),
typ: Typ::List(Box::new(Typ::Object(ObjectType::new(None, Some(vec![]))))),
default: Some(json!(null)),
has_default: true,
oidx: None

View File

@@ -4,7 +4,7 @@ use anyhow::{anyhow, bail};
use nu_parser::lex;
use serde_json::{json, Value};
use windmill_parser::{Arg, MainArgSignature, Typ};
use windmill_parser::{Arg, MainArgSignature, ObjectType, Typ};
pub fn parse_nu_signature(code: &str) -> anyhow::Result<MainArgSignature> {
let (tokens, ..) = lex(code.as_bytes(), 0, &[], &[], true);
@@ -162,8 +162,8 @@ pub fn parse_nu_signature(code: &str) -> anyhow::Result<MainArgSignature> {
"int" => Typ::Int,
"float" => Typ::Float,
"number" => Typ::Float,
"record" => Typ::Object(vec![]),
"table" => Typ::List(Box::new(Typ::Object(vec![]))),
"record" => Typ::Object(ObjectType::new(None, Some(vec![]))),
"table" => Typ::List(Box::new(Typ::Object(ObjectType::new(None, Some(vec![]))))),
"nothing" => Typ::Unknown,
// TODO: needs additional work on literal parsing
// "binary" => Typ::Bytes,

View File

@@ -1,7 +1,7 @@
#[cfg(test)]
mod test {
use serde_json::json;
use windmill_parser::{Arg, MainArgSignature, Typ};
use windmill_parser::{Arg, MainArgSignature, ObjectType, Typ};
use windmill_parser_nu::parse_nu_signature;
#[test]
@@ -200,7 +200,7 @@ mod test {
Arg {
name: "a7".into(),
otyp: None,
typ: Typ::Object(vec![]),
typ: Typ::Object(ObjectType::new(None, Some(vec![]))),
default: None,
has_default: false,
oidx: None
@@ -216,7 +216,7 @@ mod test {
Arg {
name: "a9".into(),
otyp: None,
typ: Typ::List(Box::new(Typ::Object(vec![]))),
typ: Typ::List(Box::new(Typ::Object(ObjectType::new(None, Some(vec![]))))),
default: None,
has_default: false,
oidx: None

View File

@@ -1,5 +1,5 @@
use serde_json::Value;
use windmill_parser::{to_snake_case, Arg, MainArgSignature, Typ};
use windmill_parser::{to_snake_case, Arg, MainArgSignature, ObjectType, Typ};
use php_parser_rs::parser::{
self,
@@ -18,7 +18,7 @@ fn parse_php_type(e: Type) -> Typ {
Type::Integer(_) => Typ::Int,
Type::String(_) => Typ::Str(None),
Type::Array(_) => Typ::List(Box::new(Typ::Str(None))),
Type::Object(_) => Typ::Object(vec![]),
Type::Object(_) => Typ::Object(ObjectType::new(None, Some(vec![]))),
Type::Named(_, name) => Typ::Resource(to_snake_case(name.to_string().as_ref())),
_ => Typ::Unknown,
}

View File

@@ -11,7 +11,7 @@ use std::collections::HashMap;
use itertools::Itertools;
use serde_json::json;
use windmill_parser::{json_to_typ, Arg, MainArgSignature, Typ};
use windmill_parser::{json_to_typ, Arg, MainArgSignature, ObjectType, Typ};
use rustpython_parser::{
ast::{
@@ -226,7 +226,7 @@ fn parse_typ(id: &str) -> Typ {
"float" => Typ::Float,
"int" => Typ::Int,
"bool" => Typ::Bool,
"dict" => Typ::Object(vec![]),
"dict" => Typ::Object(ObjectType::new(None, Some(vec![]))),
"list" => Typ::List(Box::new(Typ::Unknown)),
"bytes" => Typ::Bytes,
"datetime" => Typ::Datetime,

View File

@@ -14,7 +14,7 @@ use std::{
iter::Peekable,
str::CharIndices,
};
pub use windmill_parser::{Arg, MainArgSignature, Typ};
pub use windmill_parser::{Arg, MainArgSignature, ObjectType, Typ};
pub const SANITIZED_ENUM_STR: &str = "__sanitized_enum__";
pub const SANITIZED_RAW_STRING_STR: &str = "__sanitized_raw_string__";
@@ -231,7 +231,7 @@ lazy_static::lazy_static! {
static ref RE_ARG_BIGQUERY: Regex = Regex::new(r#"(?m)^-- @(\w+) \((\w+(?:\[\])?)\)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
// -- $name (type) = default
static ref RE_ARG_DUCKDB: Regex = Regex::new(r#"(?m)^-- \$(\w+) \((\w+)\)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
static ref RE_ARG_DUCKDB: Regex = Regex::new(r#"(?m)^-- \$(\w+) \(([A-Za-z0-9_\[\]]+)\)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
static ref RE_ARG_SNOWFLAKE: Regex = Regex::new(r#"(?m)^-- \? (\w+) \((\w+)\)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
@@ -737,7 +737,7 @@ pub fn parse_pg_typ(typ: &str) -> Typ {
"bigint" => Typ::Int,
"bool" | "boolean" => Typ::Bool,
"char" | "character" => Typ::Str(None),
"json" | "jsonb" => Typ::Object(vec![]),
"json" | "jsonb" => Typ::Object(ObjectType::new(None, Some(vec![]))),
"smallint" | "int2" => Typ::Int,
"smallserial" | "serial2" => Typ::Int,
"serial" | "serial4" => Typ::Int,
@@ -769,7 +769,7 @@ pub fn parse_bigquery_typ(typ: &str) -> Typ {
match typ {
"string" => Typ::Str(None),
"bytes" => Typ::Bytes,
"json" => Typ::Object(vec![]),
"json" => Typ::Object(ObjectType::new(None, Some(vec![]))),
"timestamp" | "date" | "time" | "datetime" => Typ::Datetime,
"integer" | "int64" => Typ::Int,
"float" | "float64" | "numeric" | "bignumeric" => Typ::Float,

View File

@@ -7,19 +7,20 @@
*/
// use deno_core::{serde_v8, v8, JsRuntime, RuntimeOptions};
use serde_json::Value;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
use windmill_parser::{
json_to_typ, to_snake_case, Arg, MainArgSignature, ObjectProperty, OneOfVariant, Typ,
json_to_typ, to_snake_case, Arg, MainArgSignature, ObjectProperty, ObjectType, OneOfVariant,
Typ,
};
use swc_common::{sync::Lrc, FileName, SourceMap, SourceMapper, Span, Spanned};
use swc_ecma_ast::{
ArrayLit, AssignPat, BigInt, BindingIdent, Bool, Decl, ExportDecl, Expr, FnDecl, Ident,
IdentName, Lit, MemberExpr, MemberProp, ModuleDecl, ModuleItem, Number, ObjectLit, ObjectPat,
Param, Pat, Str, TsArrayType, TsEntityName, TsKeywordType, TsKeywordTypeKind, TsLit, TsLitType,
TsOptionalType, TsParenthesizedType, TsPropertySignature, TsType, TsTypeAnn, TsTypeElement,
TsTypeLit, TsTypeRef, TsUnionOrIntersectionType, TsUnionType,
ArrayLit, AssignPat, BigInt, BindingIdent, Bool, Decl, ExportDecl, Expr, Ident, IdentName, Lit,
MemberExpr, MemberProp, ModuleDecl, ModuleItem, Number, ObjectLit, ObjectPat, Param, Pat, Stmt,
Str, TsArrayType, TsEntityName, TsInterfaceDecl, TsKeywordType, TsKeywordTypeKind, TsLit,
TsLitType, TsOptionalType, TsParenthesizedType, TsPropertySignature, TsType, TsTypeAliasDecl,
TsTypeAnn, TsTypeElement, TsTypeLit, TsTypeRef, TsUnionOrIntersectionType, TsUnionType,
};
use swc_ecma_parser::{lexer::Lexer, EsSyntax, Parser, StringInput, Syntax, TsSyntax};
@@ -32,6 +33,19 @@ struct ImportsFinder {
skip_type_only: bool,
}
impl ImportsFinder {
fn process_raw(&mut self, raw: Option<String>) {
if let Some(ref s) = raw {
let s = s.to_string();
if s.starts_with("'") && s.ends_with("'") {
self.imports.insert(s[1..s.len() - 1].to_string());
} else if s.starts_with("\"") && s.ends_with("\"") {
self.imports.insert(s[1..s.len() - 1].to_string());
}
}
}
}
impl Visit for ImportsFinder {
noop_visit_type!();
@@ -59,14 +73,47 @@ impl Visit for ImportsFinder {
}
}
}
if let Some(ref s) = n.src.raw {
let s = s.to_string();
if s.starts_with("'") && s.ends_with("'") {
self.imports.insert(s[1..s.len() - 1].to_string());
} else if s.starts_with("\"") && s.ends_with("\"") {
self.imports.insert(s[1..s.len() - 1].to_string());
self.process_raw(n.src.raw.as_ref().map(|x| x.to_string()));
}
fn visit_export_all(&mut self, node: &swc_ecma_ast::ExportAll) {
if !self.skip_type_only || node.type_only {
return;
}
self.process_raw(node.src.raw.as_ref().map(|x| x.to_string()));
}
fn visit_named_export(&mut self, node: &swc_ecma_ast::NamedExport) {
if node.src.is_none() || !self.skip_type_only || node.type_only {
return;
}
if node.specifiers.len() > 0 {
let mut is_type_only = true;
for specifier in node.specifiers.iter() {
match specifier {
swc_ecma_ast::ExportSpecifier::Named(swc_ecma_ast::ExportNamedSpecifier {
is_type_only,
..
}) if *is_type_only => (),
_ => {
is_type_only = false;
break;
}
}
}
if is_type_only {
return;
}
}
self.process_raw(
node.src
.as_ref()
.unwrap()
.raw
.as_ref()
.map(|x| x.to_string()),
);
}
}
@@ -159,6 +206,11 @@ pub fn parse_expr_for_ids(code: &str) -> anyhow::Result<Vec<(String, String)>> {
Ok(visitor.idents.into_iter().collect())
}
#[derive(Debug)]
pub enum TypeDecl {
Interface(TsInterfaceDecl),
Alias(TsTypeAliasDecl),
}
pub mod asset_parser;
pub use asset_parser::parse_assets;
@@ -168,7 +220,7 @@ pub fn parse_deno_signature(
code: &str,
skip_dflt: bool,
skip_params: bool,
main_override: Option<String>,
entrypoint_override: Option<String>,
) -> anyhow::Result<MainArgSignature> {
let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(FileName::Custom("main.ts".into()).into(), code.into());
@@ -188,6 +240,9 @@ pub fn parse_deno_signature(
err_s += &e.into_kind().msg().to_string();
}
let mut has_preprocessor = false;
let mut entrypoint_params = None;
let ast = parser
.parse_module()
.map_err(|e| {
@@ -195,35 +250,71 @@ pub fn parse_deno_signature(
})?
.body;
let has_preprocessor = ast.iter().any(|x| match x {
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
decl: Decl::Fn(FnDecl { ident: Ident { sym, .. }, .. }),
..
})) => &sym.to_string() == "preprocessor",
_ => false,
});
let entrypoint_function = entrypoint_override.as_deref().unwrap_or("main");
let main_name = main_override.unwrap_or("main".to_string());
let params = ast.into_iter().find_map(|x| match x {
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
decl: Decl::Fn(FnDecl { ident: Ident { sym, .. }, function, .. }),
..
})) if &sym.to_string() == &main_name => Some(function.params),
_ => None,
});
let mut symbol_table: HashMap<String, TypeDecl> = HashMap::new();
for item in ast {
if let ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { decl, .. }))
| ModuleItem::Stmt(Stmt::Decl(decl)) = item
{
match decl {
Decl::TsInterface(mut iface) => match symbol_table.get_mut(iface.id.sym.as_str()) {
Some(TypeDecl::Interface(interface)) => {
interface.body.body.append(&mut iface.body.body);
}
None => {
symbol_table.insert(
to_snake_case(iface.id.sym.as_str()),
TypeDecl::Interface(*iface),
);
}
_ => {}
},
Decl::TsTypeAlias(alias) => {
symbol_table.insert(
to_snake_case(alias.id.sym.as_str()),
TypeDecl::Alias(*alias),
);
}
Decl::Fn(fn_decl) => {
let name = fn_decl.ident.sym.to_string();
if name == "preprocessor" {
has_preprocessor = true;
}
if name == entrypoint_function {
entrypoint_params = Some(fn_decl.function.params.clone());
}
}
_ => {}
}
}
}
let mut c: u16 = 0;
let no_main_func = params.is_none();
let no_main_func = entrypoint_params.is_none();
let mut type_resolver = HashMap::new();
let r = MainArgSignature {
star_args: false,
star_kwargs: false,
args: if skip_params {
vec![]
} else {
params
.map(|x| {
x.into_iter()
.map(|x| parse_param(x, &cm, skip_dflt, &mut c))
entrypoint_params
.map(|param| {
param
.into_iter()
.map(|param| {
parse_param(
&symbol_table,
&mut type_resolver,
param,
&cm,
skip_dflt,
&mut c,
)
})
.collect::<anyhow::Result<Vec<Arg>>>()
})
.transpose()?
@@ -236,14 +327,16 @@ pub fn parse_deno_signature(
}
fn parse_param(
x: Param,
symbol_table: &HashMap<String, TypeDecl>,
type_resolver: &mut HashMap<String, (Typ, bool)>,
param: Param,
cm: &Lrc<SourceMap>,
skip_dflt: bool,
counter: &mut u16,
) -> anyhow::Result<Arg> {
let r = match x.pat {
let r = match param.pat {
Pat::Ident(ident) => {
let (name, typ, nullable) = binding_ident_to_arg(&ident);
let (name, typ, nullable) = binding_ident_to_arg(symbol_table, type_resolver, &ident);
Ok(Arg {
otyp: None,
name,
@@ -256,9 +349,9 @@ fn parse_param(
// Pat::Object(ObjectPat { ... }) = todo!()
Pat::Assign(AssignPat { left, right, .. }) => {
let (name, mut typ, _nullable) = match *left {
Pat::Ident(ident) => binding_ident_to_arg(&ident),
Pat::Ident(ident) => binding_ident_to_arg(symbol_table, type_resolver, &ident),
Pat::Object(ObjectPat { type_ann, .. }) => {
let (typ, nullable) = eval_type_ann(&type_ann);
let (typ, nullable) = eval_type_ann(symbol_table, type_resolver, &type_ann);
*counter += 1;
let name = format!("anon{}", counter);
(name, typ, nullable)
@@ -300,16 +393,16 @@ fn parse_param(
Ok(Arg { otyp: None, name, typ, default: dflt, has_default: true, oidx: None })
}
Pat::Object(ObjectPat { type_ann, .. }) => {
let (typ, nullable) = eval_type_ann(&type_ann);
let (typ, nullable) = eval_type_ann(symbol_table, type_resolver, &type_ann);
*counter += 1;
let name = format!("anon{}", counter);
Ok(Arg { otyp: None, name, typ, default: None, has_default: nullable, oidx: None })
}
_ => Err(anyhow::anyhow!(
"parameter syntax unsupported: `{}`: {:#?}",
cm.span_to_snippet(x.span())
.unwrap_or_else(|_| cm.span_to_string(x.span())),
x.pat
cm.span_to_snippet(param.span())
.unwrap_or_else(|_| cm.span_to_string(param.span())),
param.pat
)),
};
r
@@ -328,14 +421,22 @@ fn eval_span(span: Span, cm: &Lrc<SourceMap>) -> Option<Value> {
}
}
fn eval_type_ann(type_ann: &Option<Box<TsTypeAnn>>) -> (Typ, bool) {
fn eval_type_ann(
symbol_table: &HashMap<String, TypeDecl>,
type_resolver: &mut HashMap<String, (Typ, bool)>,
type_ann: &Option<Box<TsTypeAnn>>,
) -> (Typ, bool) {
return type_ann
.as_ref()
.map(|x| tstype_to_typ(&*x.type_ann))
.map(|x| tstype_to_typ(symbol_table, type_resolver, &*x.type_ann, true))
.unwrap_or((Typ::Unknown, false));
}
fn binding_ident_to_arg(BindingIdent { id, type_ann }: &BindingIdent) -> (String, Typ, bool) {
let (typ, nullable) = eval_type_ann(type_ann);
fn binding_ident_to_arg(
symbol_table: &HashMap<String, TypeDecl>,
type_resolver: &mut HashMap<String, (Typ, bool)>,
BindingIdent { id, type_ann }: &BindingIdent,
) -> (String, Typ, bool) {
let (typ, nullable) = eval_type_ann(symbol_table, type_resolver, type_ann);
(id.sym.to_string(), typ, nullable)
}
@@ -365,11 +466,193 @@ pub fn remove_pinned_imports(code: &str) -> anyhow::Result<String> {
Ok(content)
}
fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
fn resolve_type_ref(type_resolver: &HashMap<String, (Typ, bool)>, typ: &mut Typ) {
let mut visited = std::collections::HashSet::new();
resolve_type_ref_with_visited(type_resolver, typ, &mut visited);
}
fn resolve_type_ref_with_visited(
type_resolver: &HashMap<String, (Typ, bool)>,
typ: &mut Typ,
visited: &mut std::collections::HashSet<String>,
) {
match typ {
Typ::Object(ObjectType { props: Some(obj), .. }) => {
for property in obj.iter_mut() {
resolve_type_ref_with_visited(type_resolver, &mut property.typ, visited);
}
}
Typ::List(list) => resolve_type_ref_with_visited(type_resolver, list, visited),
Typ::Object(ObjectType { name: Some(name), props: None }) => {
if visited.contains(name) {
return;
}
let maybe_resolved_type = type_resolver
.get(name)
.map(|rs_typ| {
let mut typ = rs_typ.0.clone();
visited.insert(name.clone());
resolve_type_ref_with_visited(type_resolver, &mut typ, visited);
visited.remove(name);
typ
})
.unwrap_or(Typ::Object(ObjectType::new(Some(name.to_owned()), None)));
*typ = maybe_resolved_type;
}
_ => {}
}
}
fn resolve_interface(
iface: &TsInterfaceDecl,
symbol_table: &HashMap<String, TypeDecl>,
type_resolver: &mut HashMap<String, (Typ, bool)>,
) -> Vec<ObjectProperty> {
let mut properties = vec![];
for ext in &iface.extends {
// If the current interface extends other interfaces,
// retrieve their properties first and add them to the current interface's object definition.
if let Expr::Ident(Ident { sym, .. }) = &*ext.expr {
if let Some(TypeDecl::Interface(parent_iface)) =
symbol_table.get(&to_snake_case(sym.as_str()))
{
properties.extend(resolve_interface(parent_iface, symbol_table, type_resolver));
}
}
}
for member in &iface.body.body {
if let TsTypeElement::TsPropertySignature(sig) = member {
if let Expr::Ident(Ident { sym, .. }) = &*sig.key {
let typ = sig
.type_ann
.as_ref()
.map(|ta| {
Box::new(tstype_to_typ(symbol_table, type_resolver, &ta.type_ann, false).0)
})
.unwrap_or(Box::new(Typ::Unknown));
properties.push(ObjectProperty { key: sym.to_string(), typ });
}
}
}
properties
}
fn resolve_type_alias(
alias: &TsTypeAliasDecl,
symbol_table: &HashMap<String, TypeDecl>,
type_resolver: &mut HashMap<String, (Typ, bool)>,
top_level_call: bool,
) -> (Typ, bool) {
tstype_to_typ(symbol_table, type_resolver, &alias.type_ann, top_level_call)
}
fn resolve_ts_interface_and_type_alias(
type_name: &str,
symbol_table: &HashMap<String, TypeDecl>,
type_resolver: &mut HashMap<String, (Typ, bool)>,
top_level_call: bool,
) -> Option<(Typ, bool)> {
let Some(type_declaration) = symbol_table.get(type_name) else {
return None;
};
if let Some(resolved_type) = type_resolver.get_mut(type_name) {
return Some(resolved_type.to_owned());
}
type_resolver.insert(
type_name.to_owned(),
(
Typ::Object(ObjectType::new(Some(type_name.to_owned()), None)),
false,
),
);
let mut resolved_type = match type_declaration {
TypeDecl::Alias(alias) => {
resolve_type_alias(alias, symbol_table, type_resolver, top_level_call)
}
TypeDecl::Interface(iface) => (
Typ::Object(ObjectType::new(
Some(to_snake_case(&type_name)),
Some(resolve_interface(iface, symbol_table, type_resolver)),
)),
false,
),
};
fn update_name(typ: &mut Typ, type_name: &str) {
match typ {
// OBJECT TYPES:
// Normally, when you declare something like:
// type Postgresql = object
// it will initially resolve to:
// Typ::Object(ObjectType { name: None, .. })
//
// If the object has no name (name == None), we assign `type_name`
// so it becomes:
// Typ::Object(ObjectType { name: Some("postgresql"), .. })
Typ::Object(ObjectType { name, .. }) if name.is_none() => {
*name = Some(type_name.to_owned());
}
// RESOURCE TYPES:
// In some cases, when a type alias points to something unresolved:
// type Postgresql = Foo
// might initially resolve to:
// Typ::Resource("Foo")
//
// This ensures backwards compatibility by updating that resource
// name to the alias name, so it becomes:
// Typ::Resource("postgresql")
Typ::Resource(name) if name != type_name => {
*name = type_name.to_owned();
}
// LIST TYPES:
// For lists, e.g. `type PostgresqlList = [object]`,
// we recursively call `update_name` on the inner type,
// so the same renaming logic applies inside the list.
Typ::List(list) => {
update_name(list, type_name);
}
_ => {}
}
}
update_name(&mut resolved_type.0, type_name);
type_resolver.insert(type_name.to_owned(), resolved_type.clone());
// `top_level_call` indicates whether the current invocation of the function
// is at the topmost level (e.g., the immediate parameters of the main function).
// When true:
// - Type references within object properties (e.g., nested interfaces) are recursively resolved
// up to a default depth to inline and fully materialize their structure.
if top_level_call {
resolve_type_ref(type_resolver, &mut resolved_type.0);
}
Some(resolved_type)
}
fn tstype_to_typ(
symbol_table: &HashMap<String, TypeDecl>,
type_resolver: &mut HashMap<String, (Typ, bool)>,
ts_type: &TsType,
top_level_call: bool,
) -> (Typ, bool) {
match ts_type {
TsType::TsKeywordType(t) => (
match t.kind {
TsKeywordTypeKind::TsObjectKeyword => Typ::Object(vec![]),
TsKeywordTypeKind::TsObjectKeyword => {
Typ::Object(ObjectType::new(None, Some(vec![])))
}
TsKeywordTypeKind::TsBooleanKeyword => Typ::Bool,
TsKeywordTypeKind::TsBigIntKeyword => Typ::Int,
TsKeywordTypeKind::TsNumberKeyword => Typ::Float,
@@ -391,7 +674,17 @@ fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
key: sym.to_string(),
typ: type_ann
.as_ref()
.map(|typ| Box::new(tstype_to_typ(&*typ.type_ann).0))
.map(|typ| {
Box::new(
tstype_to_typ(
symbol_table,
type_resolver,
&*typ.type_ann,
top_level_call,
)
.0,
)
})
.unwrap_or(Box::new(Typ::Unknown)),
}),
_ => None,
@@ -399,21 +692,25 @@ fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
_ => None,
})
.collect();
(Typ::Object(properties), false)
(Typ::Object(ObjectType::new(None, Some(properties))), false)
}
TsType::TsParenthesizedType(TsParenthesizedType { type_ann, .. }) => {
tstype_to_typ(type_ann)
tstype_to_typ(symbol_table, type_resolver, type_ann, top_level_call)
}
// TODO: we can do better here and extract the inner type of array
TsType::TsArrayType(TsArrayType { elem_type, .. }) => {
(Typ::List(Box::new(tstype_to_typ(&**elem_type).0)), false)
}
TsType::TsArrayType(TsArrayType { elem_type, .. }) => (
Typ::List(Box::new(
tstype_to_typ(symbol_table, type_resolver, &**elem_type, top_level_call).0,
)),
false,
),
TsType::TsLitType(TsLitType { lit: TsLit::Str(Str { value, .. }), .. }) => {
(Typ::Str(Some(vec![value.to_string()])), false)
}
TsType::TsOptionalType(TsOptionalType { type_ann, .. }) => {
(tstype_to_typ(type_ann).0, true)
}
TsType::TsOptionalType(TsOptionalType { type_ann, .. }) => (
tstype_to_typ(symbol_table, type_resolver, type_ann, top_level_call).0,
true,
),
TsType::TsUnionOrIntersectionType(TsUnionOrIntersectionType::TsUnionType(
TsUnionType { types, .. },
)) => {
@@ -438,11 +735,18 @@ fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
} else {
0
};
(tstype_to_typ(&types[other_p]).0, true)
(
tstype_to_typ(symbol_table, type_resolver, &types[other_p], top_level_call).0,
true,
)
} else {
if types.len() > 1 {
let one_of_values: Vec<OneOfVariant> =
types.into_iter().map_while(parse_one_of_type).collect();
let one_of_values: Vec<OneOfVariant> = types
.into_iter()
.map_while(|t| {
parse_one_of_type(symbol_table, type_resolver, t, top_level_call)
})
.collect();
if one_of_values.len() == types.len() {
return (Typ::OneOf(one_of_values), false);
@@ -481,6 +785,7 @@ fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
TsEntityName::Ident(Ident { sym, .. }) => sym,
TsEntityName::TsQualifiedName(p) => &*p.right.sym,
};
match sym.to_string().as_str() {
"Resource" => (
Typ::Resource(
@@ -501,24 +806,68 @@ fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
"Base64" => (Typ::Bytes, false),
"Email" => (Typ::Email, false),
"Sql" => (Typ::Sql, false),
x @ _ if x.starts_with("DynSelect_") => (
Typ::DynSelect(x.strip_prefix("DynSelect_").unwrap().to_string()),
symbol @ _ if symbol.starts_with("DynSelect_") => (
Typ::DynSelect(symbol.strip_prefix("DynSelect_").unwrap().to_string()),
false,
),
x @ _ => (Typ::Resource(to_snake_case(x)), false),
symbol @ _ => {
let symbol = to_snake_case(symbol);
resolve_ts_interface_and_type_alias(
&symbol,
symbol_table,
type_resolver,
top_level_call,
)
.unwrap_or_else(|| (Typ::Resource(symbol), false))
}
}
}
_ => (Typ::Unknown, false),
}
}
fn parse_one_of_type(x: &Box<TsType>) -> Option<OneOfVariant> {
fn parse_one_of_type(
symbol_table: &HashMap<String, TypeDecl>,
type_resolver: &mut HashMap<String, (Typ, bool)>,
x: &Box<TsType>,
top_level_call: bool,
) -> Option<OneOfVariant> {
match &**x {
TsType::TsTypeLit(TsTypeLit { members, .. }) => {
let label = one_of_label(members)?;
let properties = one_of_properties(members);
let properties =
one_of_properties(symbol_table, type_resolver, members, top_level_call);
Some(OneOfVariant { label, properties })
}
TsType::TsTypeRef(TsTypeRef { type_name, .. }) => {
let label = type_name.as_ident()?.sym.to_string();
match label.as_str() {
symbol
if ["Resource", "Date", "Base64", "Email", "Sql"]
.iter()
.any(|s| *s == symbol)
|| symbol.starts_with("DynSelect_") =>
{
return None
}
symbol @ _ => {
let Typ::Object(ObjectType { props: Some(properties), .. }) =
resolve_ts_interface_and_type_alias(
symbol,
symbol_table,
type_resolver,
top_level_call,
)
.unwrap_or_else(|| (Typ::Resource(to_snake_case(symbol)), false))
.0
else {
return None;
};
Some(OneOfVariant { label, properties })
}
}
}
_ => None,
}
}
@@ -550,7 +899,12 @@ fn one_of_label(members: &Vec<TsTypeElement>) -> Option<String> {
})
}
fn one_of_properties(members: &Vec<TsTypeElement>) -> Vec<ObjectProperty> {
fn one_of_properties(
symbol_table: &HashMap<String, TypeDecl>,
type_resolver: &mut HashMap<String, (Typ, bool)>,
members: &Vec<TsTypeElement>,
top_level_call: bool,
) -> Vec<ObjectProperty> {
members
.iter()
.filter_map(|x| {
@@ -564,10 +918,12 @@ fn one_of_properties(members: &Vec<TsTypeElement>) -> Vec<ObjectProperty> {
};
let typ = type_ann
.as_ref()
.map(|typ| Box::new(tstype_to_typ(&*typ.type_ann).0))
.unwrap_or(Box::new(Typ::Unknown));
.map(|typ| {
tstype_to_typ(symbol_table, type_resolver, &*typ.type_ann, top_level_call).0
})
.unwrap_or(Typ::Unknown);
Some(ObjectProperty { key: sym.to_string(), typ })
Some(ObjectProperty { key: sym.to_string(), typ: Box::new(typ) })
})
.collect()
}

View File

@@ -1,8 +1,11 @@
#[cfg(test)]
mod tests {
use windmill_parser_ts::parse_expr_for_imports;
use serde_json::json;
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, ObjectType, Typ};
use windmill_parser_ts::{parse_deno_signature, parse_expr_for_imports};
#[test]
fn test_imports() {
fn test_imports_basic() {
let code = r#"
import { foo } from "bar";
import type { foo } from "bar2";
@@ -12,8 +15,645 @@ mod tests {
import { type foo, type bar } from "bar4";
import * as foo from "bar5";
import foo from "bar6";
export * from "bar8";
export { foo } from "bar9";
export { type foo } from "bar10";
export { foo, type bar } from "bar11";
export { type foo, type bar } from "bar12";
export { type foo, bar } from "bar13";
export { bar, type foo } from "bar14";
export type { foo } from "bar15";
"#;
let imports = parse_expr_for_imports(code, true).unwrap();
assert_eq!(imports, vec!["bar", "bar3", "bar5", "bar6", "bar7"]);
assert_eq!(
imports,
["bar", "bar11", "bar13", "bar14", "bar3", "bar5", "bar6", "bar7", "bar8", "bar9"]
);
}
#[test]
fn test_parse_empty_main_signature() {
let code = r#"
export async function main() {
return "Hello World";
}
"#;
let sig = parse_deno_signature(code, false, false, None).unwrap();
assert_eq!(
sig,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![],
no_main_func: Some(false),
has_preprocessor: Some(false),
}
);
}
#[test]
fn test_parse_basic_types() {
let code = r#"
export async function main(
str_param: string,
num_param: number,
bool_param: boolean,
any_param: any
) {
return { str_param, num_param, bool_param, any_param };
}
"#;
let sig = parse_deno_signature(code, false, false, None).unwrap();
assert_eq!(
sig,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
name: "str_param".to_string(),
otyp: None,
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None,
},
Arg {
name: "num_param".to_string(),
otyp: None,
typ: Typ::Float,
default: None,
has_default: false,
oidx: None,
},
Arg {
name: "bool_param".to_string(),
otyp: None,
typ: Typ::Bool,
default: None,
has_default: false,
oidx: None,
},
Arg {
name: "any_param".to_string(),
otyp: None,
typ: Typ::Unknown,
default: None,
has_default: false,
oidx: None,
},
],
no_main_func: Some(false),
has_preprocessor: Some(false),
}
);
}
#[test]
fn test_parse_with_defaults() {
let code = r#"
export async function main(
name: string = "World",
count: number = 42,
enabled: boolean = true
) {
return { name, count, enabled };
}
"#;
let sig = parse_deno_signature(code, false, false, None).unwrap();
assert_eq!(
sig,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
name: "name".to_string(),
otyp: None,
typ: Typ::Str(None),
default: Some(json!("World")),
has_default: true,
oidx: None,
},
Arg {
name: "count".to_string(),
otyp: None,
typ: Typ::Float,
default: Some(json!(42)),
has_default: true,
oidx: None,
},
Arg {
name: "enabled".to_string(),
otyp: None,
typ: Typ::Bool,
default: Some(json!(true)),
has_default: true,
oidx: None,
},
],
no_main_func: Some(false),
has_preprocessor: Some(false),
}
);
}
#[test]
fn test_parse_array_types() {
let code = r#"
export async function main(
strings: string[],
numbers: number[],
items: any[]
) {
return { strings, numbers, items };
}
"#;
let sig = parse_deno_signature(code, false, false, None).unwrap();
assert_eq!(
sig,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
name: "strings".to_string(),
otyp: None,
typ: Typ::List(Box::new(Typ::Str(None))),
default: None,
has_default: false,
oidx: None,
},
Arg {
name: "numbers".to_string(),
otyp: None,
typ: Typ::List(Box::new(Typ::Float)),
default: None,
has_default: false,
oidx: None,
},
Arg {
name: "items".to_string(),
otyp: None,
typ: Typ::List(Box::new(Typ::Unknown)),
default: None,
has_default: false,
oidx: None,
},
],
no_main_func: Some(false),
has_preprocessor: Some(false),
}
);
}
#[test]
fn test_parse_resource_types() {
let code = r#"
type Postgresql = Resource;
export async function main(
database: Postgresql
) {
return { database };
}
"#;
let sig = parse_deno_signature(code, false, false, None).unwrap();
assert_eq!(
sig,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![Arg {
name: "database".to_string(),
otyp: None,
typ: Typ::Resource("postgresql".to_string()),
default: None,
has_default: false,
oidx: None,
},],
no_main_func: Some(false),
has_preprocessor: Some(false),
}
);
}
#[test]
fn test_parse_array_resource_types() {
let code = r#"
type Postgresql = Foo;
export async function main(
database: Postgresql[]
) {
return { database };
}
"#;
let sig = parse_deno_signature(code, false, false, None).unwrap();
assert_eq!(
sig,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![Arg {
name: "database".to_string(),
otyp: None,
typ: Typ::List(Box::new(Typ::Resource("postgresql".to_string()))),
default: None,
has_default: false,
oidx: None,
},],
no_main_func: Some(false),
has_preprocessor: Some(false),
}
);
}
#[test]
fn test_parse_interface_object() {
let code = r#"
interface UserConfig {
name: string;
age: number;
active?: boolean;
}
export async function main(config: UserConfig) {
return config;
}
"#;
let sig = parse_deno_signature(code, false, false, None).unwrap();
assert_eq!(
sig,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![Arg {
name: "config".to_string(),
otyp: None,
typ: Typ::Object(ObjectType::new(
Some("user_config".to_string()),
Some(vec![
ObjectProperty::new("name".to_string(), Box::new(Typ::Str(None))),
ObjectProperty::new("age".to_string(), Box::new(Typ::Float)),
ObjectProperty::new("active".to_string(), Box::new(Typ::Bool)),
])
)),
default: None,
has_default: false,
oidx: None,
}],
no_main_func: Some(false),
has_preprocessor: Some(false),
}
);
}
#[test]
fn test_parse_array_of_objects() {
let code = r#"
interface Item {
id: string;
value: number;
}
export async function main(items: Item[]) {
return items;
}
"#;
let sig = parse_deno_signature(code, false, false, None).unwrap();
assert_eq!(
sig,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![Arg {
name: "items".to_string(),
otyp: None,
typ: Typ::List(Box::new(Typ::Object(ObjectType::new(
Some("item".to_string()),
Some(vec![
ObjectProperty::new("id".to_string(), Box::new(Typ::Str(None))),
ObjectProperty::new("value".to_string(), Box::new(Typ::Float)),
])
)))),
default: None,
has_default: false,
oidx: None,
}],
no_main_func: Some(false),
has_preprocessor: Some(false),
}
);
}
#[test]
fn test_parse_nested_objects() {
let code = r#"
interface Address {
street: string;
city: string;
}
interface Person {
name: string;
address: Address;
}
export async function main(person: Person) {
return person;
}
"#;
let sig = parse_deno_signature(code, false, false, None).unwrap();
assert_eq!(
sig,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![Arg {
name: "person".to_string(),
otyp: None,
typ: Typ::Object(ObjectType::new(
Some("person".to_string()),
Some(vec![
ObjectProperty::new("name".to_string(), Box::new(Typ::Str(None))),
ObjectProperty::new(
"address".to_string(),
Box::new(Typ::Object(ObjectType::new(
Some("address".to_string()),
Some(vec![
ObjectProperty::new(
"street".to_string(),
Box::new(Typ::Str(None))
),
ObjectProperty::new(
"city".to_string(),
Box::new(Typ::Str(None))
),
])
)))
),
])
)),
default: None,
has_default: false,
oidx: None,
}],
no_main_func: Some(false),
has_preprocessor: Some(false),
}
);
}
#[test]
fn test_parse_builtin_types() {
let code = r#"
export async function main(
date_param: Date,
base64_param: Base64,
email_param: Email,
sql_param: Sql
) {
return { date_param, base64_param, email_param, sql_param };
}
"#;
let sig = parse_deno_signature(code, false, false, None).unwrap();
assert_eq!(
sig,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
name: "date_param".to_string(),
otyp: None,
typ: Typ::Datetime,
default: None,
has_default: false,
oidx: None,
},
Arg {
name: "base64_param".to_string(),
otyp: None,
typ: Typ::Bytes,
default: None,
has_default: false,
oidx: None,
},
Arg {
name: "email_param".to_string(),
otyp: None,
typ: Typ::Email,
default: None,
has_default: false,
oidx: None,
},
Arg {
name: "sql_param".to_string(),
otyp: None,
typ: Typ::Sql,
default: None,
has_default: false,
oidx: None,
},
],
no_main_func: Some(false),
has_preprocessor: Some(false),
}
);
}
#[test]
fn test_parse_optional_parameters() {
let code = r#"
export async function main(
required: string,
optional?: number,
with_default: boolean = false
) {
return { required, optional, with_default };
}
"#;
let sig = parse_deno_signature(code, false, false, None).unwrap();
assert_eq!(
sig,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
name: "required".to_string(),
otyp: None,
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None,
},
Arg {
name: "optional".to_string(),
otyp: None,
typ: Typ::Float,
default: None,
has_default: true,
oidx: None,
},
Arg {
name: "with_default".to_string(),
otyp: None,
typ: Typ::Bool,
default: Some(json!(false)),
has_default: true,
oidx: None,
},
],
no_main_func: Some(false),
has_preprocessor: Some(false),
}
);
}
#[test]
fn test_parse_complex_interface() {
let code = r#"
interface Config {
timeout: number;
retries: number;
}
export async function main(
config: Config,
tags: string[]
) {
return { config, tags };
}
"#;
let sig = parse_deno_signature(code, false, false, None).unwrap();
assert_eq!(sig.args.len(), 2);
assert_eq!(sig.args[0].name, "config");
assert_eq!(sig.args[0].has_default, false);
assert!(matches!(sig.args[0].typ, Typ::Object(_)));
assert_eq!(sig.args[1].name, "tags");
assert_eq!(sig.args[1].has_default, false);
assert!(matches!(sig.args[1].typ, Typ::List(_)));
}
#[test]
fn test_parse_no_main_function() {
let code = r#"
function helper() {
return "helper";
}
const value = 42;
"#;
let sig = parse_deno_signature(code, false, false, None).unwrap();
assert_eq!(
sig,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![],
no_main_func: Some(true),
has_preprocessor: Some(false),
}
);
}
#[test]
fn test_parse_custom_entrypoint() {
let code = r#"
export async function customMain(param: string) {
return param;
}
"#;
let sig = parse_deno_signature(code, false, false, Some("customMain".to_string())).unwrap();
assert_eq!(
sig,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![Arg {
name: "param".to_string(),
otyp: None,
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None,
}],
no_main_func: Some(false),
has_preprocessor: Some(false),
}
);
}
#[test]
fn test_parse_dyn_select() {
let code = r#"
export async function main(
selector: DynSelect_my_options
) {
return selector;
}
"#;
let sig = parse_deno_signature(code, false, false, None).unwrap();
assert_eq!(
sig,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![Arg {
name: "selector".to_string(),
otyp: None,
typ: Typ::DynSelect("my_options".to_string()),
default: None,
has_default: false,
oidx: None,
}],
no_main_func: Some(false),
has_preprocessor: Some(false),
}
);
}
#[test]
fn test_parse_invalid_typescript() {
let code = r#"
this is not valid typescript code {
"#;
let result = parse_deno_signature(code, false, false, None);
assert!(result.is_err());
}
#[test]
fn test_parse_with_preprocessor() {
let code = r#"
export async function preprocessor() {
return "preprocessed";
}
export async function main(param: string) {
return param;
}
"#;
let sig = parse_deno_signature(code, false, false, None).unwrap();
assert_eq!(
sig,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![Arg {
name: "param".to_string(),
otyp: None,
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None,
}],
no_main_func: Some(false),
has_preprocessor: Some(true),
}
);
}
}

View File

@@ -1,79 +0,0 @@
#!/usr/bin/env bash
set -eou pipefail
#-# bun and deno
OUT_DIR="../../../cli/wasm/ts"
mkdir -p $OUT_DIR
wasm-pack build --release --target deno --out-dir $OUT_DIR --features "ts-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
# sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-ts"/' $OUT_DIR/package.json
rm $OUT_DIR/.gitignore
#-# sql languages, graphql and bash/powershell, since they all use regex
OUT_DIR="../../../cli/wasm/regex"
mkdir -p $OUT_DIR
wasm-pack build --release --target deno --out-dir $OUT_DIR \
--features "sql-parser,graphql-parser,bash-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
# sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-regex"/' $OUT_DIR/package.json
rm $OUT_DIR/.gitignore
#-# python
OUT_DIR="../../../cli/wasm/python"
mkdir -p $OUT_DIR
wasm-pack build --release --target deno --out-dir $OUT_DIR --features "py-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
# sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-py"/' $OUT_DIR/package.json
rm $OUT_DIR/.gitignore
#-# go
OUT_DIR="../../../cli/wasm/go"
mkdir -p $OUT_DIR
wasm-pack build --release --target deno --out-dir $OUT_DIR --features "go-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
# sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-go"/' $OUT_DIR/package.json
rm $OUT_DIR/.gitignore
#-# php
OUT_DIR="../../../cli/wasm/php"
mkdir -p $OUT_DIR
wasm-pack build --release --target deno --out-dir $OUT_DIR --features "php-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
# sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-php"/' $OUT_DIR/package.json
rm $OUT_DIR/.gitignore
#-# rust
OUT_DIR="../../../cli/wasm/rust"
mkdir -p $OUT_DIR
wasm-pack build --release --target deno --out-dir $OUT_DIR --features "rust-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
# sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-rust"/' $OUT_DIR/package.json
rm $OUT_DIR/.gitignore
#-# ansible
OUT_DIR="../../../cli/wasm/yaml"
mkdir -p $OUT_DIR
wasm-pack build --release --target deno --out-dir $OUT_DIR --features "ansible-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
# sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-yaml"/' $OUT_DIR/package.json
rm $OUT_DIR/.gitignore
#-# C# (needs some more stuff to compile C tree sitter into wasm)
OUT_DIR="../../../cli/wasm/csharp"
mkdir -p $OUT_DIR
CFLAGS_wasm32_unknown_unknown="-I$(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin" RUSTFLAGS="-Zwasm-c-abi=spec" wasm-pack build --release --target deno --out-dir $OUT_DIR --features "csharp-parser"
# sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-csharp"/' $OUT_DIR/package.json
rm $OUT_DIR/.gitignore
#-# Nu
OUT_DIR="../../../cli/wasm/nu"
mkdir -p $OUT_DIR
wasm-pack build --release --target deno --out-dir $OUT_DIR --features "nu-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
rm $OUT_DIR/.gitignore
#-# Java
OUT_DIR="../../../cli/wasm/java"
mkdir -p $OUT_DIR
CFLAGS_wasm32_unknown_unknown="-I$(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin" RUSTFLAGS="-Zwasm-c-abi=spec" wasm-pack build --release --target deno --out-dir $OUT_DIR --features "java-parser"
rm $OUT_DIR/.gitignore

View File

@@ -1,68 +0,0 @@
#!/bin/bash
set -eou pipefail
#-# full pkg
OUT_DIR="pkg"
wasm-pack build --release --target web --out-dir $OUT_DIR --all-features \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
#-# bun and deno
OUT_DIR="pkg-ts"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "ts-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-ts"/' $OUT_DIR/package.json
#-# sql languages, graphql and bash/powershell, since they all use regex
OUT_DIR="pkg-regex"
wasm-pack build --release --target web --out-dir $OUT_DIR \
--features "sql-parser,graphql-parser,bash-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-regex"/' $OUT_DIR/package.json
#-# python
OUT_DIR="pkg-py"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "py-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-py"/' $OUT_DIR/package.json
#-# go
OUT_DIR="pkg-go"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "go-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-go"/' $OUT_DIR/package.json
#-# php
OUT_DIR="pkg-php"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "php-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-php"/' $OUT_DIR/package.json
#-# rust
OUT_DIR="pkg-rust"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "rust-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-rust"/' $OUT_DIR/package.json
#-# ansible
OUT_DIR="pkg-yaml"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "ansible-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-yaml"/' $OUT_DIR/package.json
#-# C# (needs some more stuff to compile C tree sitter into wasm)
# TODO: hasn't been tested on mac, might need fixing
OUT_DIR="pkg-csharp"
CFLAGS_wasm32_unknown_unknown="-I$(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin" RUSTFLAGS="-Zwasm-c-abi=spec" wasm-pack build --release --target web --out-dir $OUT_DIR --features "csharp-parser"
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-csharp"/' $OUT_DIR/package.json
#-# nu
# TODO: hasn't been tested on mac, might need fixing
OUT_DIR="pkg-nu"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "nu-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-nu"/' $OUT_DIR/package.json
#-# Java (needs some more stuff to compile C tree sitter into wasm)
OUT_DIR="pkg-java"
CFLAGS_wasm32_unknown_unknown="-I$(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin" RUSTFLAGS="-Zwasm-c-abi=spec" wasm-pack build --release --target web --out-dir $OUT_DIR --features "java-parser"
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-java"/' $OUT_DIR/package.json

View File

@@ -1,66 +0,0 @@
#!/usr/bin/env bash
set -eou pipefail
#-# full pkg
OUT_DIR="pkg"
wasm-pack build --release --target web --out-dir $OUT_DIR --all-features \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
#-# bun and deno
OUT_DIR="pkg-ts"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "ts-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-ts"/' $OUT_DIR/package.json
#-# sql languages, graphql and bash/powershell, since they all use regex
OUT_DIR="pkg-regex"
wasm-pack build --release --target web --out-dir $OUT_DIR \
--features "sql-parser,graphql-parser,bash-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-regex"/' $OUT_DIR/package.json
#-# python
OUT_DIR="pkg-py"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "py-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-py"/' $OUT_DIR/package.json
#-# go
OUT_DIR="pkg-go"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "go-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-go"/' $OUT_DIR/package.json
#-# php
OUT_DIR="pkg-php"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "php-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-php"/' $OUT_DIR/package.json
#-# rust
OUT_DIR="pkg-rust"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "rust-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-rust"/' $OUT_DIR/package.json
#-# ansible
OUT_DIR="pkg-yaml"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "ansible-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-yaml"/' $OUT_DIR/package.json
#-# C# (needs some more stuff to compile C tree sitter into wasm)
OUT_DIR="pkg-csharp"
CFLAGS_wasm32_unknown_unknown="-I$(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin" RUSTFLAGS="-Zwasm-c-abi=spec" wasm-pack build --release --target web --out-dir $OUT_DIR --features "csharp-parser"
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-csharp"/' $OUT_DIR/package.json
#-# Nu
OUT_DIR="pkg-nu"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "nu-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-nu"/' $OUT_DIR/package.json
#-# Java (needs some more stuff to compile C tree sitter into wasm)
OUT_DIR="pkg-java"
CFLAGS_wasm32_unknown_unknown="-I$(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin" RUSTFLAGS="-Zwasm-c-abi=spec" wasm-pack build --release --target web --out-dir $OUT_DIR --features "java-parser"
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-java"/' $OUT_DIR/package.json

View File

@@ -0,0 +1,132 @@
#!/usr/bin/env nu
const targets = [
{
ident: "ts", # OUT_DIR inferred from this
desc: "Bun and Deno"
features: "ts-parser",
env: "default", # available: default, tree-sitter
}, {
ident: "regex",
desc: "Sql languages, Graphql and Bash/Powershell"
features: "sql-parser,graphql-parser,bash-parser",
env: "default",
}, {
ident: "py",
desc: "Python",
features: "py-parser",
env: "default",
}, {
ident: "go",
desc: "Go (Go-lang)",
features: "go-parser",
env: "default",
}, {
ident: "php",
desc: "Php",
features: "php-parser",
env: "default",
}, {
ident: "rust",
desc: "Rust",
features: "rust-parser",
env: "default",
}, {
ident: "yaml",
desc: "Ansible",
features: "ansible-parser",
env: "default",
}, {
ident: "csharp",
desc: "C#",
features: "csharp-parser",
env: "tree-sitter",
}, {
ident: "nu",
desc: "Nu (Nushell)",
features: "nu-parser",
env: "tree-sitter",
}, {
ident: "java",
desc: "Java",
features: "java-parser",
env: "tree-sitter",
# }, {
# ident: "ruby",
# desc: "Ruby",
# features: "ruby-parser",
# env: "tree-sitter",
},
# ^^^ Add new entry here ^^^
];
# NOTE: This is legacy command for building all, but it is not more used
# #-# full pkg
# OUT_DIR="pkg"
# wasm-pack build --release --target web --out-dir $OUT_DIR --all-features \
# -Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
# Build all separately
def 'main all' [ --no-opt(-n), --cli] {
if ($no_opt) {
if ($cli) {
$targets | each { main $in.ident -n --cli }
} else {
$targets | each { main $in.ident -n }
}
} else {
if ($cli) {
$targets | each { main $in.ident --cli }
} else {
$targets | each { main $in.ident }
}
}
}
# Build specific language
def main [
target: string, # Language, e.g.: py, ts, rust
--no-opt(-n), # Compile in debug mode for dev
--cli # Compile and place binaries in cli.
] {
let t = $targets | where ident == $target;
if ($t | is-empty) {
let pretty = $targets | each {|t| $"($t.ident) - ($t.desc)"} | str join "\n";
panic $"Target '($target)' does not exist\n\nAvailable targets:\n($pretty)"
} else {
let t = $t | get 0;
mut profile = "";
mut tar = "";
if ($cli) {
$env.OUT_DIR = $"../../../cli/wasm/($t.ident)"
$tar = "deno"
} else {
$env.OUT_DIR = $"pkg-($t.ident)"
$tar = "web"
}
if ($no_opt) {
$profile = "--no-opt"
} else {
$profile = "--release"
}
print $"Building in ($profile) mode ($env.OUT_DIR)"
match $t.env {
"default" => {
wasm-pack build ($profile) --target ($tar) --out-dir $env.OUT_DIR --features ($t.features) -Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
},
"tree-sitter" => {
$env.CFLAGS_wasm32_unknown_unknown = $"-I(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin"
$env.RUSTFLAGS = "-Zwasm-c-abi=spec"
wasm-pack build ($profile) --target ($tar) --out-dir $env.OUT_DIR --features $t.features
},
_ => { panic $"Unknown env template: ($t.env)" },
}
if ($cli) {
rm $"($env.OUT_DIR)/.gitignore"
} else {
let p = $"($env.OUT_DIR)/package.json"
open $p | update name $"windmill-parser-wasm-($t.ident)" | save -f $p
}
}
}

View File

@@ -4,22 +4,9 @@
# and perform installation to frontend
def "main" [
lang: string # Example: nu
--release(-r)
] {
let out_dir = $'pkg-($lang)'
if $release {
open build-pkgs.sh
| split row '#-'
| find $out_dir
| bash -c $"RUST_LOG=trace ($in.0)"
} else {
open build-pkgs.sh
| split row '#-'
| find $out_dir
| str replace "--release" "--no-opt"
| bash -c $"WASM_OPT=-Oz ($in.0)"
}
./build.nu $lang --no-opt
(
cd ../../../frontend; npm install ../backend/parsers/windmill-parser-wasm/($out_dir)
cd ../../../frontend; npm install ../backend/parsers/windmill-parser-wasm/pkg-($lang)
)
}

View File

@@ -30,3 +30,6 @@ popd
pushd "pkg-nu" && npm publish ${args}
popd
pushd "pkg-java" && npm publish ${args}
popd

View File

@@ -1,6 +1,6 @@
use serde_json::json;
use wasm_bindgen_test::wasm_bindgen_test;
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, Typ};
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, ObjectType, Typ};
use windmill_parser_bash::parse_powershell_sig;
use windmill_parser_ts::{parse_deno_signature, parse_expr_for_ids, parse_expr_for_imports};
@@ -115,10 +115,10 @@ export function main(test1?: string, test2: string = \"burkina\",
Arg {
otyp: None,
name: "min_object".to_string(),
typ: Typ::Object(vec![
typ: Typ::Object(ObjectType::new(None, Some(vec![
ObjectProperty { key: "a".to_string(), typ: Box::new(Typ::Str(None)) },
ObjectProperty { key: "b".to_string(), typ: Box::new(Typ::Float) }
]),
]))),
default: None,
has_default: false,
oidx: None
@@ -210,10 +210,10 @@ export function main(test2 = \"burkina\",
Arg {
otyp: None,
name: "min_object".to_string(),
typ: Typ::Object(vec![
typ: Typ::Object(ObjectType::new(None, Some(vec![
ObjectProperty { key: "a".to_string(), typ: Box::new(Typ::Str(None)) },
ObjectProperty { key: "b".to_string(), typ: Box::new(Typ::Int) }
]),
]))),
default: Some(json!({"a": "test", "b": 42})),
has_default: true,
oidx: None

View File

@@ -2,7 +2,7 @@ use std::collections::HashMap;
use anyhow::anyhow;
use serde_json::json;
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, Typ};
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, ObjectType, Typ};
use yaml_rust::{Yaml, YamlEmitter, YamlLoader};
pub fn parse_ansible_sig(inner_content: &str) -> anyhow::Result<MainArgSignature> {
@@ -147,9 +147,9 @@ fn parse_ansible_typ(arg: &Yaml) -> Typ {
})
}
}
Typ::Object(prop_vec)
Typ::Object(ObjectType::new(None, Some(prop_vec)))
} else {
Typ::Object(vec![])
Typ::Object(ObjectType::new(None, Some(vec![])))
}
}
"array" => {

View File

@@ -28,6 +28,24 @@ pub struct ObjectProperty {
pub typ: Box<Typ>,
}
impl ObjectProperty {
pub fn new(key: String, typ: Box<Typ>) -> ObjectProperty {
ObjectProperty { key, typ }
}
}
#[derive(Serialize, Clone, Debug, PartialEq)]
pub struct ObjectType {
pub name: Option<String>,
pub props: Option<Vec<ObjectProperty>>,
}
impl ObjectType {
pub fn new(name: Option<String>, props: Option<Vec<ObjectProperty>>) -> ObjectType {
ObjectType { name, props }
}
}
#[derive(Serialize, Clone, Debug, PartialEq)]
#[serde(rename_all(serialize = "lowercase"))]
pub struct OneOfVariant {
@@ -49,7 +67,7 @@ pub enum Typ {
Email,
Sql,
DynSelect(String),
Object(Vec<ObjectProperty>),
Object(ObjectType),
OneOf(Vec<OneOfVariant>),
Unknown,
}
@@ -70,11 +88,14 @@ pub fn json_to_typ(js: &Value) -> Typ {
Value::Number(n) if n.is_i64() => Typ::Int,
Value::Number(_) => Typ::Float,
Value::Bool(_) => Typ::Bool,
Value::Object(o) => Typ::Object(
o.iter()
.map(|(k, v)| ObjectProperty { key: k.to_string(), typ: Box::new(json_to_typ(v)) })
.collect(),
),
Value::Object(o) => Typ::Object(ObjectType::new(
None,
Some(
o.iter()
.map(|(k, v)| ObjectProperty { key: k.to_string(), typ: Box::new(json_to_typ(v)) })
.collect(),
),
)),
Value::Array(a) => Typ::List(Box::new(a.first().map(json_to_typ).unwrap_or(Typ::Unknown))),
_ => Typ::Unknown,
}

View File

@@ -54,8 +54,8 @@ use windmill_common::{
stats_oss::schedule_stats,
triggers::TriggerKind,
utils::{
create_default_worker_suffix, create_ssh_agent_worker_suffix, worker_name_with_suffix,
Mode, GIT_VERSION, HOSTNAME, MODE_AND_ADDONS,
create_default_worker_suffix, worker_name_with_suffix, Mode, GIT_VERSION, HOSTNAME,
MODE_AND_ADDONS,
},
worker::{
reload_custom_tags_setting, Connection, HUB_CACHE_DIR, TMP_DIR, TMP_LOGS_DIR, WORKER_GROUP,
@@ -365,7 +365,7 @@ async fn windmill_main() -> anyhow::Result<()> {
"Creating http client for cluster using base internal url {}",
std::env::var("BASE_INTERNAL_URL").unwrap_or_default()
);
let suffix = create_ssh_agent_worker_suffix(&hostname);
let suffix = create_default_worker_suffix(&hostname);
(
Connection::Http(build_agent_http_client(&suffix)),
Some(suffix),

View File

@@ -65,7 +65,8 @@ use windmill_common::{
users::truncate_token,
utils::{empty_as_none, now_from_db, rd_string, report_critical_error, Mode},
worker::{
load_env_vars, load_init_bash_from_env, load_whitelist_env_vars_from_env,
load_env_vars, load_init_bash_from_env, load_periodic_bash_script_from_env,
load_periodic_bash_script_interval_from_env, load_whitelist_env_vars_from_env,
load_worker_config, reload_custom_tags_setting, store_pull_query,
store_suspended_pull_query, update_min_version, Connection, WorkerConfig,
DEFAULT_TAGS_PER_WORKSPACE, DEFAULT_TAGS_WORKSPACES, INDEXER_CONFIG, SCRIPT_TOKEN_EXPIRY,
@@ -213,6 +214,8 @@ pub async fn initial_load(
priority_tags_sorted: vec![],
dedicated_worker: None,
init_bash: load_init_bash_from_env(),
periodic_script_bash: load_periodic_bash_script_from_env(),
periodic_script_interval_seconds: load_periodic_bash_script_interval_from_env(),
cache_clear: None,
additional_python_paths: None,
pip_local_dependencies: None,
@@ -1574,6 +1577,17 @@ pub async fn reload_worker_config(db: &DB, tx: KillpillSender, kill_if_change: b
tracing::error!("Error cleaning the cache: {e:#}");
}
}
if (*wc).periodic_script_bash != config.periodic_script_bash {
tracing::info!("Periodic script bash config changed, sending killpill. Expecting to be restarted by supervisor.");
let _ = tx.send();
}
if (*wc).periodic_script_interval_seconds != config.periodic_script_interval_seconds
{
tracing::info!("Periodic script interval config changed, sending killpill. Expecting to be restarted by supervisor.");
let _ = tx.send();
}
}
drop(wc);
@@ -1835,12 +1849,14 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
);
}
let jobs = sqlx::query_as::<_, QueuedJob>("SELECT * FROM v2_as_queue WHERE id = ANY($1)")
.bind(&timeouts[..])
.fetch_all(db)
.await
.map_err(|e| tracing::error!("Error fetching same worker jobs: {:?}", e))
.unwrap_or_default();
let jobs = sqlx::query_as::<_, QueuedJob>(
"SELECT *, null as workflow_as_code_status FROM v2_as_queue WHERE id = ANY($1)",
)
.bind(&timeouts[..])
.fetch_all(db)
.await
.map_err(|e| tracing::error!("Error fetching same worker jobs: {:?}", e))
.unwrap_or_default();
jobs
};
@@ -1848,7 +1864,7 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
let non_restartable_jobs = if *RESTART_ZOMBIE_JOBS {
vec![]
} else {
sqlx::query_as::<_, QueuedJob>("SELECT * FROM v2_as_queue WHERE last_ping < now() - ($1 || ' seconds')::interval
sqlx::query_as::<_, QueuedJob>("SELECT *, null as workflow_as_code_status FROM v2_as_queue WHERE last_ping < now() - ($1 || ' seconds')::interval
AND running = true AND job_kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlescriptflow') AND same_worker = false")
.bind(ZOMBIE_JOB_TIMEOUT.as_str())
.fetch_all(db)
@@ -1873,13 +1889,14 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
}
}
let zombie_jobs_restart_limit_reached =
sqlx::query_as::<_, QueuedJob>("SELECT * FROM v2_as_queue WHERE id = ANY($1)")
.bind(&zombie_jobs_uuid_restart_limit_reached[..])
.fetch_all(db)
.await
.ok()
.unwrap_or_else(|| vec![]);
let zombie_jobs_restart_limit_reached = sqlx::query_as::<_, QueuedJob>(
"SELECT *, null as workflow_as_code_status FROM v2_as_queue WHERE id = ANY($1)",
)
.bind(&zombie_jobs_uuid_restart_limit_reached[..])
.fetch_all(db)
.await
.ok()
.unwrap_or_else(|| vec![]);
let timeouts = non_restartable_jobs
.into_iter()

View File

@@ -27,8 +27,10 @@ use serde::Serialize;
use windmill_common::flows::InputTransform;
use windmill_common::worker::WORKER_CONFIG;
#[cfg(feature = "python")]
use windmill_common::flow_status::{FlowStatus, FlowStatusModule, RestartedFrom};
use windmill_common::{
flow_status::{FlowStatus, FlowStatusModule, RestartedFrom},
flows::{FlowModule, FlowModuleValue, FlowValue},
jobs::{JobKind, JobPayload, RawCode},
jwt::JWT_SECRET,
@@ -114,12 +116,14 @@ fn next_worker_name() -> String {
.unwrap_or(s)
})
.unwrap_or("no thread name");
format!("{id}/{thread_name}")
format!("{id}/worker-{thread_name}")
}
pub struct ApiServer {
pub addr: std::net::SocketAddr,
#[allow(unused)]
tx: tokio::sync::broadcast::Sender<()>,
#[allow(unused)]
task: tokio::task::JoinHandle<anyhow::Result<()>>,
}
@@ -153,6 +157,7 @@ impl ApiServer {
Self { addr, tx, task }
}
#[allow(unused)]
async fn close(self) -> anyhow::Result<()> {
println!("closing api server");
let Self { tx, task, .. } = self;
@@ -193,11 +198,13 @@ async fn set_jwt_secret() -> () {
}
mod suspend_resume {
#[cfg(feature = "deno_core")]
use serde_json::json;
#[cfg(feature = "deno_core")]
use super::*;
#[cfg(feature = "deno_core")]
async fn wait_until_flow_suspends(
flow: Uuid,
mut queue: impl Stream<Item = Uuid> + Unpin,
@@ -218,6 +225,7 @@ mod suspend_resume {
}
}
#[cfg(feature = "deno_core")]
fn flow() -> FlowValue {
serde_json::from_value(serde_json::json!({
"modules": [{
@@ -462,12 +470,12 @@ mod suspend_resume {
}
}
#[cfg(feature = "deno_core")]
mod retry {
use super::*;
use serde_json::json;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use super::*;
/// test helper provides some external state to help steps fail at specific points
struct Server {
addr: std::net::SocketAddr,
@@ -536,6 +544,7 @@ def main(last, port):
"#
}
#[cfg(feature = "deno_core")]
fn flow_forloop_retry() -> FlowValue {
serde_json::from_value(serde_json::json!({
"modules": [{
@@ -1073,6 +1082,7 @@ async fn listen_for_completed_jobs(db: &Pool<Postgres>) -> impl Stream<Item = Uu
listen_for_uuid_on(db, "completed").await
}
#[cfg(feature = "deno_core")]
async fn listen_for_queue(db: &Pool<Postgres>) -> impl Stream<Item = Uuid> + Unpin {
listen_for_uuid_on(db, "queued").await
}

View File

@@ -32,7 +32,7 @@ http_trigger = ["dep:matchit", "dep:thiserror", "dep:sha1", "dep:constant_time_e
static_frontend = ["dep:rust-embed"]
postgres_trigger = ["dep:rust-postgres", "dep:pg_escape", "dep:byteorder", "dep:thiserror", "dep:rust_decimal", "dep:rust-postgres-native-tls"]
mqtt_trigger = ["dep:thiserror", "dep:rumqttc"]
sqs_trigger = ["dep:aws-sdk-sqs", "dep:thiserror", "dep:aws-config"]
sqs_trigger = ["dep:aws-sdk-sqs", "dep:aws-sdk-sts", "dep:aws-sdk-sso", "dep:aws-sdk-ssooidc", "dep:thiserror", "dep:aws-config", "dep:backon"]
deno_core = ["dep:deno_core", "dep:deno_error"]
gcp_trigger = ["dep:thiserror", "dep:google-cloud-pubsub", "dep:google-cloud-googleapis", "dep:tonic"]
cloud = ["windmill-common/cloud"]
@@ -138,13 +138,17 @@ rust_decimal = { workspace = true, optional = true }
rust-postgres-native-tls = { workspace = true, optional = true}
rumqttc = { workspace = true, optional = true }
aws-sdk-sqs = { workspace = true, optional = true }
aws-config = { workspace = true, optional = true }
aws-sdk-sso = { workspace = true, optional = true }
aws-sdk-ssooidc = { workspace = true, optional = true }
aws-sdk-sts = { workspace = true, optional = true }
rustls = { workspace = true }
aws-config = { workspace = true, optional = true }
google-cloud-pubsub = { workspace = true, optional = true }
google-cloud-googleapis = { workspace = true , optional = true }
tonic = { workspace = true, optional = true }
deno_error = { workspace = true, optional = true }
deno_core = { workspace = true, optional = true }
backon = {workspace = true, optional = true}
[build-dependencies]
deno_core = { workspace = true, optional = true }

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.509.1
version: 1.515.1
title: Windmill API
contact:
@@ -3413,6 +3413,9 @@ paths:
cc_client_secret:
type: string
description: "OAuth client secret for resource-level credentials (client_credentials flow only)"
cc_token_url:
type: string
description: "OAuth token URL override for resource-level authentication (client_credentials flow only)"
required:
- expires_in
- client
@@ -3455,6 +3458,9 @@ paths:
cc_client_secret:
type: string
description: "OAuth client secret for resource-level authentication"
cc_token_url:
type: string
description: "OAuth token URL override for resource-level authentication"
required:
- cc_client_id
- cc_client_secret
@@ -5090,6 +5096,10 @@ paths:
in: query
schema:
type: boolean
- name: authed
in: query
schema:
type: boolean
responses:
"200":
description: script details
@@ -7664,7 +7674,9 @@ paths:
progress:
type: integer
flow_status:
$ref: "#/components/schemas/WorkflowStatusRecord"
$ref: "../../openflow.openapi.yaml#/components/schemas/FlowStatus"
workflow_as_code_status:
$ref: "#/components/schemas/WorkflowStatus"
/w/{workspace}/jobs_u/getupdate_sse/{id}:
get:
@@ -14056,6 +14068,8 @@ components:
the execution of this script will be permissioned_as and by extension its DT_TOKEN.
flow_status:
$ref: "../../openflow.openapi.yaml#/components/schemas/FlowStatus"
workflow_as_code_status:
$ref: "#/components/schemas/WorkflowStatus"
raw_flow:
$ref: "../../openflow.openapi.yaml#/components/schemas/FlowValue"
is_flow_step:
@@ -14163,6 +14177,8 @@ components:
the execution of this script will be permissioned_as and by extension its DT_TOKEN.
flow_status:
$ref: "../../openflow.openapi.yaml#/components/schemas/FlowStatus"
workflow_as_code_status:
$ref: "#/components/schemas/WorkflowStatus"
raw_flow:
$ref: "../../openflow.openapi.yaml#/components/schemas/FlowValue"
is_flow_step:
@@ -14625,36 +14641,41 @@ components:
- type: object
properties:
object:
type: array
items:
type: object
properties:
key:
type: string
typ:
oneOf:
- type: string
enum:
[
"float",
"int",
"bool",
"email",
"unknown",
"bytes",
"dict",
"datetime",
"sql",
]
- type: object
properties:
str: {}
required: [str]
required:
- key
- typ
type: object
properties:
name:
type: string
props:
type: array
items:
type: object
properties:
key:
type: string
typ:
oneOf:
- type: string
enum:
[
"float",
"int",
"bool",
"email",
"unknown",
"bytes",
"dict",
"datetime",
"sql",
]
- type: object
properties:
str: {}
required: [str]
required:
- key
- typ
required:
- object
- object
- type: object
properties:
list:

View File

@@ -18,10 +18,11 @@ use windmill_audit::audit_oss::audit_log;
use windmill_audit::ActionKind;
use windmill_common::{
error::{self},
worker::MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS,
DB,
};
use crate::{db::ApiAuthed, utils::{require_devops_role}};
use crate::{db::ApiAuthed, utils::require_devops_role};
pub fn global_service() -> Router {
Router::new()
@@ -129,6 +130,42 @@ async fn update_config(
));
}
if name.starts_with("worker__") {
let periodic_script_bash = config
.get("periodic_script_bash")
.filter(|v| !v.is_null())
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty());
let periodic_script_interval = config
.get("periodic_script_interval_seconds")
.filter(|v| !v.is_null());
match (periodic_script_bash, periodic_script_interval) {
(Some(_), Some(interval_value)) => {
if let Some(interval) = interval_value.as_u64() {
if interval < MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS {
return Err(error::Error::BadRequest(format!(
"Periodic script interval must be at least {} seconds, got {} seconds",
MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS, interval
)));
}
} else {
return Err(error::Error::BadRequest(
"Periodic script interval must be a valid number".to_string(),
));
}
}
(Some(_), None) => {
return Err(error::Error::BadRequest(
"Periodic script interval must be specified when periodic script is configured"
.to_string(),
));
}
_ => {}
}
}
let mut tx = db.begin().await?;
sqlx::query!(
"INSERT INTO config (name, config) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET config = $2",

View File

@@ -18,6 +18,7 @@ use quick_cache::sync::Cache;
use serde_json::value::RawValue;
use sqlx::Pool;
use std::collections::HashMap;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
use std::time::Instant;
@@ -723,7 +724,7 @@ macro_rules! get_job_query {
CASE WHEN jsonb_typeof(args) = 'object' THEN args
ELSE jsonb_build_object('value', args)
END
ELSE '{{\"reason\": \"WINDMILL_TOO_BIG\"}}'::jsonb END as args, COALESCE(flow_status, workflow_as_code_status) AS flow_status, \
ELSE '{{\"reason\": \"WINDMILL_TOO_BIG\"}}'::jsonb END as args, flow_status, workflow_as_code_status, \
{logs} as logs, {code} as raw_code, canceled_by is not null as canceled, canceled_by, canceled_reason, kind as job_kind, \
CASE WHEN trigger_kind = 'schedule'::job_trigger_kind THEN trigger END AS schedule_path, permissioned_as, \
{flow} as raw_flow, flow_step_id IS NOT NULL AS is_flow_step, script_lang as language, \
@@ -3003,6 +3004,7 @@ impl<'a> From<UnifiedJob> for Job {
result_columns: None,
logs: None,
flow_status: None,
workflow_as_code_status: None,
deleted: uj.deleted,
canceled: uj.canceled,
canceled_by: uj.canceled_by,
@@ -3039,6 +3041,7 @@ impl<'a> From<UnifiedJob> for Job {
scheduled_for: uj.scheduled_for.unwrap(),
logs: None,
flow_status: None,
workflow_as_code_status: None,
canceled: uj.canceled,
canceled_by: uj.canceled_by,
canceled_reason: None,
@@ -3076,7 +3079,7 @@ struct CancelJob {
reason: Option<String>,
}
#[derive(Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
enum PreviewKind {
Code,
@@ -3087,7 +3090,7 @@ enum PreviewKind {
ScriptHash,
}
#[derive(Deserialize)]
#[derive(Debug, Deserialize)]
struct Preview {
content: Option<String>,
kind: Option<PreviewKind>,
@@ -4461,6 +4464,9 @@ pub async fn run_wait_result_flow_by_path_get(
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
let path = flow_path.to_path();
check_scopes(&authed, || format!("jobs:run:flows:{path}"))?;
if method == http::Method::HEAD {
return Ok(Json(serde_json::json!("")).into_response());
}
@@ -4483,7 +4489,7 @@ pub async fn run_wait_result_flow_by_path_get(
.to_args_from_runnable(
&db,
&w_id,
RunnableId::from_flow_path(flow_path.to_path()),
RunnableId::from_flow_path(path),
run_query.skip_preprocessor,
)
.await?;
@@ -4503,12 +4509,15 @@ pub async fn run_wait_result_script_by_path(
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
let path = script_path.to_path();
check_scopes(&authed, || format!("jobs:run:scripts:{path}"))?;
let args = args
.to_args_from_runnable(
&authed,
&db,
&w_id,
RunnableId::from_script_path(script_path.to_path()),
RunnableId::from_script_path(path),
run_query.skip_preprocessor,
)
.await?;
@@ -4526,14 +4535,11 @@ pub async fn run_wait_result_script_by_path_internal(
w_id: String,
args: PushArgsOwned,
) -> error::Result<Response> {
let script_path = script_path.to_path();
check_scopes(&authed, || format!("jobs:run:scripts:{script_path}"))?;
check_queue_too_long(&db, QUEUE_LIMIT_WAIT_RESULT.or(run_query.queue_limit)).await?;
let mut tx = user_db.clone().begin(&authed).await?;
let (job_payload, tag, delete_after_use, timeout, on_behalf_of) =
script_path_to_payload(script_path, &mut *tx, &w_id, run_query.skip_preprocessor).await?;
script_path_to_payload(script_path.to_path(), &mut *tx, &w_id, run_query.skip_preprocessor).await?;
drop(tx);
let tag = run_query.tag.clone().or(tag);
@@ -4716,12 +4722,15 @@ pub async fn run_wait_result_flow_by_path(
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
let path = flow_path.to_path();
check_scopes(&authed, || format!("jobs:run:flows:{path}"))?;
let args = args
.to_args_from_runnable(
&authed,
&db,
&w_id,
RunnableId::from_flow_path(flow_path.to_path()),
RunnableId::from_flow_path(path),
run_query.skip_preprocessor,
)
.await?;
@@ -4742,7 +4751,6 @@ pub async fn run_wait_result_flow_by_path_internal(
check_queue_too_long(&db, run_query.queue_limit).await?;
let flow_path = flow_path.to_path();
check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?;
let scheduled_for = run_query.get_scheduled_for(&db).await?;
@@ -5686,25 +5694,31 @@ pub struct JobUpdate {
pub mem_peak: Option<i32>,
pub progress: Option<i32>,
pub flow_status: Option<Box<serde_json::value::RawValue>>,
pub workflow_as_code_status: Option<Box<serde_json::value::RawValue>>,
pub job: Option<Job>,
pub only_result: Option<Box<serde_json::value::RawValue>>,
}
#[derive(PartialEq)]
pub struct JobUpdateLastStatus {
pub running: Option<bool>,
pub completed: Option<bool>,
pub log_offset: Option<i32>,
pub mem_peak: Option<i32>,
impl JobUpdate {
pub fn hash_str(&self) -> String {
let mut hasher = DefaultHasher::new();
self.hash(&mut hasher);
format!("{:x}", hasher.finish())
}
}
impl From<&JobUpdate> for JobUpdateLastStatus {
fn from(update: &JobUpdate) -> Self {
Self {
running: update.running,
completed: update.completed,
log_offset: update.log_offset,
mem_peak: update.mem_peak,
impl Hash for JobUpdate {
fn hash<H: Hasher>(&self, state: &mut H) {
self.running.hash(state);
self.completed.hash(state);
self.log_offset.hash(state);
self.mem_peak.hash(state);
self.progress.hash(state);
if !self.completed.unwrap_or(false) {
self.flow_status.as_ref().map(|x| x.get().hash(state));
self.workflow_as_code_status
.as_ref()
.map(|x| x.get().hash(state));
}
}
}
@@ -5852,7 +5866,7 @@ fn get_job_update_sse_stream(
tokio::spawn(async move {
let mut log_offset = initial_log_offset;
let mut last_update: Option<JobUpdateLastStatus> = None;
let mut last_update_hash: Option<String> = None;
// Send initial update immediately
let mut running = running;
@@ -5872,7 +5886,7 @@ fn get_job_update_sse_stream(
.await
{
Ok(update) => {
last_update = Some((&update).into());
last_update_hash = Some(update.hash_str());
let completion_sent = update.completed.unwrap_or(false);
if running.is_some() && update.running.is_some_and(|x| x) {
running = Some(true);
@@ -5951,9 +5965,9 @@ fn get_job_update_sse_stream(
// if !only_result.unwrap_or(false) {
// tracing::error!("update {:?}", update);
// }
let update_last_status = (&update).into();
let update_last_status = update.hash_str();
// Only send if the update has changed
if last_update.as_ref() != Some(&update_last_status) {
if last_update_hash.as_ref() != Some(&update_last_status) {
// Update log offset if available
if let Some(new_offset) = update.log_offset {
log_offset = Some(new_offset);
@@ -5966,7 +5980,7 @@ fn get_job_update_sse_stream(
break;
}
last_update = Some(update_last_status);
last_update_hash = Some(update_last_status);
}
}
Err(_) => {
@@ -6068,6 +6082,7 @@ async fn get_job_update_data(
progress: None,
job: None,
flow_status: None,
workflow_as_code_status: None,
only_result: result.0,
})
} else {
@@ -6080,22 +6095,8 @@ async fn get_job_update_data(
END AS running,
SUBSTR(logs, GREATEST($1 - log_offset, 0)) AS logs,
COALESCE(r.memory_peak, c.memory_peak) AS mem_peak,
CASE
-- flow step:
WHEN flow_step_id IS NOT NULL THEN NULL
-- completed:
WHEN c.id IS NOT NULL THEN COALESCE(
c.workflow_as_code_status || c.flow_status,
c.workflow_as_code_status,
c.flow_status
)
-- not completed:
ELSE COALESCE(
f.workflow_as_code_status || f.flow_status,
f.workflow_as_code_status,
f.flow_status
)
END AS \"flow_status: sqlx::types::Json<Box<RawValue>>\",
COALESCE(c.flow_status, f.flow_status) AS \"flow_status: sqlx::types::Json<Box<RawValue>>\",
COALESCE(c.workflow_as_code_status, f.workflow_as_code_status) AS \"workflow_as_code_status: sqlx::types::Json<Box<RawValue>>\",
job_logs.log_offset + CHAR_LENGTH(job_logs.logs) + 1 AS log_offset,
created_by AS \"created_by!\",
CASE WHEN $4::BOOLEAN THEN (
@@ -6140,6 +6141,9 @@ async fn get_job_update_data(
new_logs: record.logs,
mem_peak: record.mem_peak,
progress: record.progress,
workflow_as_code_status: record
.workflow_as_code_status
.map(|x: sqlx::types::Json<Box<RawValue>>| x.0),
job,
flow_status: record
.flow_status

View File

@@ -611,7 +611,7 @@ pub async fn run_server(
.nest("/mqtt_triggers", mqtt_triggers_service)
.nest("/sqs_triggers", sqs_triggers_service)
.nest("/gcp_triggers", gcp_triggers_service)
.nest("/postgres_triggers", postgres_triggers_service),
.nest("/postgres_triggers", postgres_triggers_service),
)
.nest("/workspaces", workspaces::global_service())
.nest(
@@ -669,7 +669,7 @@ pub async fn run_server(
.layer(from_extractor::<OptAuthed>())
.layer(cors.clone()),
)
.nest("/mcp/w/:workspace_id/sse", mcp_router)
.nest("/mcp/w/:workspace_id/sse", mcp_router.layer(from_extractor::<ApiAuthed>()))
.layer(from_extractor::<OptAuthed>())
.nest("/agent_workers", {
#[cfg(feature = "agent_worker_server")]

View File

@@ -230,6 +230,20 @@ impl Runner {
Self {}
}
fn check_scopes(authed: &ApiAuthed) -> Result<(), Error> {
let scopes = authed.scopes.as_ref();
if scopes.is_none()
|| scopes
.unwrap()
.iter()
.all(|scope| scope != "mcp:all" && scope != "mcp:favorites" && !scope.starts_with("mcp:hub:"))
{
tracing::error!("Unauthorized: missing mcp scope");
return Err(Error::internal_error("Unauthorized: missing mcp scope".to_string(), None));
}
Ok(())
}
async fn get_item_schema(
path: &str,
user_db: &UserDB,
@@ -854,14 +868,6 @@ impl ServerHandler for Runner {
request: CallToolRequestParam,
context: RequestContext<RoleServer>,
) -> Result<CallToolResult, Error> {
let parse_args = |args_opt: Option<JsonObject>| -> Result<Value, Error> {
args_opt.map(Value::Object).ok_or_else(|| {
Error::invalid_params(
"Missing arguments for tool",
Some(request.name.clone().into()),
)
})
};
let http_parts = context
.extensions
@@ -875,15 +881,22 @@ impl ServerHandler for Runner {
tracing::error!("ApiAuthed Axum extension not found");
Error::internal_error("ApiAuthed Axum extension not found", None)
})?;
Runner::check_scopes(authed)?;
let db = http_parts.extensions.get::<DB>().ok_or_else(|| {
tracing::error!("DB Axum extension not found");
Error::internal_error("DB Axum extension not found", None)
})?;
let user_db = http_parts.extensions.get::<UserDB>().ok_or_else(|| {
tracing::error!("UserDB Axum extension not found");
Error::internal_error("UserDB Axum extension not found", None)
})?;
let args = parse_args(request.arguments)?;
let args = request.arguments.map(Value::Object).ok_or_else(|| {
Error::invalid_params("Missing arguments for tool", Some(request.name.clone().into()))
})?;
let workspace_id = http_parts
.extensions
@@ -1007,6 +1020,13 @@ impl ServerHandler for Runner {
Error::internal_error("http::request::Parts not found", None)
})?;
let authed = http_parts.extensions.get::<ApiAuthed>().ok_or_else(|| {
tracing::error!("ApiAuthed Axum extension not found");
Error::internal_error("ApiAuthed Axum extension not found", None)
})?;
Runner::check_scopes(authed)?;
let db = http_parts.extensions.get::<DB>().ok_or_else(|| {
tracing::error!("DB Axum extension not found");
Error::internal_error("DB Axum extension not found", None)
@@ -1017,11 +1037,6 @@ impl ServerHandler for Runner {
Error::internal_error("UserDB Axum extension not found", None)
})?;
let authed = http_parts.extensions.get::<ApiAuthed>().ok_or_else(|| {
tracing::error!("ApiAuthed Axum extension not found");
Error::internal_error("ApiAuthed Axum extension not found", None)
})?;
let workspace_id = http_parts
.extensions
.get::<WorkspaceId>()
@@ -1031,15 +1046,13 @@ impl ServerHandler for Runner {
})
.map(|w_id| w_id.0.clone())?;
let owned_scope = authed.scopes.as_ref().and_then(|scopes| {
let scopes = authed.scopes.as_ref();
let owned_scope = scopes.and_then(|scopes| {
scopes
.iter()
.find(|scope| scope.starts_with("mcp:") && !scope.contains("hub"))
});
let hub_scope = authed
.scopes
.as_ref()
.and_then(|scopes| scopes.iter().find(|scope| scope.starts_with("mcp:hub")));
let hub_scope = scopes.and_then(|scopes| scopes.iter().find(|scope| scope.starts_with("mcp:hub")));
let scope_type = owned_scope.map_or("all", |scope| {
let parts = scope.split(":").collect::<Vec<&str>>();
parts[1]

View File

@@ -372,7 +372,7 @@ impl PostgresConfig {
}
}
fn retrieve_info(&self) -> PgInfo {
fn retrieve_info(&self) -> PgInfo<'_> {
let postgres_resource_path;
let publication_name;
let replication_slot_name;

View File

@@ -283,6 +283,7 @@ pub enum ScopeDomain {
Acls, // Granular access control lists
RawApps, // Raw application data
AgentWorkers, // Agent workers management
Mcp, // MCP
}
impl ScopeDomain {
@@ -328,6 +329,7 @@ impl ScopeDomain {
Self::Indexer => "indexer",
Self::Teams => "teams",
Self::GitSync => "git_sync",
Self::Mcp => "mcp",
}
}
@@ -373,6 +375,7 @@ impl ScopeDomain {
"acls" => Some(Self::Acls),
"raw_apps" => Some(Self::RawApps),
"agent_workers" => Some(Self::AgentWorkers),
"mcp" => Some(Self::Mcp),
_ => None,
}
}
@@ -415,7 +418,8 @@ impl ScopeAction {
}
}
pub fn check_route_access(
pub fn
check_route_access(
token_scopes: &[String],
route_path: &str,
http_method: &str,
@@ -424,7 +428,13 @@ pub fn check_route_access(
let required_action = map_http_method_to_action(http_method, route_path);
// Find the domain and kind for this route
let (required_domain, required_kind) = extract_domain_from_route(route_path)?;
let (required_domain, required_kind, route_suffix) = extract_domain_from_route(route_path)?;
// Backward compatibility: MCP handlers expect unusual scope actions: all, favorites, hub.
if required_domain == ScopeDomain::Mcp {
return Ok(());
}
let mut is_scoped_token = false;
// Check if any token scope grants the required access
for scope_str in token_scopes {
@@ -435,6 +445,7 @@ pub fn check_route_access(
required_domain,
required_action,
required_kind.as_deref(),
route_suffix.as_deref(),
)? {
return Ok(());
}
@@ -527,30 +538,36 @@ fn determine_kind_from_route(route_path: &str) -> Option<String> {
None
}
fn extract_domain_from_route(route_path: &str) -> Result<(ScopeDomain, Option<String>)> {
fn extract_domain_from_route(
route_path: &str,
) -> Result<(ScopeDomain, Option<String>, Option<String>)> {
// Examples:
// - /api/w/workspace/jobs/123 -> jobs domain (workspaced)
// - /api/teams/sync -> teams domain (global)
// - /api/srch/index/search -> indexer domain (global)
let parts: Vec<&str> = route_path.split('/').collect();
let (domain, kind) = if parts.len() >= 5 && parts[1] == "api" && parts[2] == "w" {
let (domain, kind, route_suffix) = if parts.len() >= 5 && parts[1] == "api" && parts[2] == "w" {
let domain_part = parts[4];
let route_suffix = &parts[4..].join("/");
let domain = ScopeDomain::from_str(domain_part);
let kind = determine_kind_from_route(route_suffix);
let kind = determine_kind_from_route(&route_suffix);
(domain, kind)
(domain, kind, Some(route_suffix.to_owned()))
} else if parts.len() >= 3 && parts[1] == "api" {
(ScopeDomain::from_str(parts[2]), None)
(
ScopeDomain::from_str(parts[2]),
None,
Some(parts[2..].join("/")),
)
} else {
(None, None)
(None, None, None)
};
if let Some(domain) = domain {
return Ok((domain, kind));
return Ok((domain, kind, route_suffix));
}
Err(Error::BadRequest(format!(
@@ -559,11 +576,34 @@ fn extract_domain_from_route(route_path: &str) -> Result<(ScopeDomain, Option<St
)))
}
const RUN_WHITELISTED_GET_PATHS: [&'static str; 19] = [
"jobs_u/get_flow/",
"jobs_u/get_root_job_id/",
"jobs_u/get/",
"jobs_u/get_logs/",
"jobs_u/get_args/",
"jobs_u/get_flow_debug_info/",
"jobs_u/completed/get/",
"jobs_u/completed/get_result/",
"jobs_u/completed/get_result_maybe/",
"jobs_u/getupdate/",
"jobs_u/getupdate_sse/",
"jobs_u/get_log_file/",
"jobs/result_by_id/",
"jobs/resume_urls/",
"jobs/flow/user_states/",
"jobs/job_signature/",
"jobs/completed/get/",
"jobs/completed/get_result/",
"jobs/completed/get_result_maybe/",
];
fn scope_grants_access(
scope: &ScopeDefinition,
required_domain: ScopeDomain,
required_action: ScopeAction,
required_kind: Option<&str>,
route_path: Option<&str>,
) -> Result<bool> {
// Check domain match
let scope_domain = ScopeDomain::from_str(&scope.domain)
@@ -577,11 +617,19 @@ fn scope_grants_access(
let scope_action = ScopeAction::from_str(&scope.action)
.ok_or_else(|| Error::BadRequest(format!("Invalid scope action: {}", scope.action)))?;
if !scope_action.includes(&required_action) {
if !scope_action.includes(&required_action)
&& !(scope_domain == ScopeDomain::Jobs
&& required_action == ScopeAction::Read
&& route_path.is_some_and(|p| {
RUN_WHITELISTED_GET_PATHS
.iter()
.any(|path| p.starts_with(path))
}))
{
return Ok(false);
}
if required_domain == ScopeDomain::Jobs && required_action == ScopeAction::Run {
if scope_domain == ScopeDomain::Jobs && required_action == ScopeAction::Run {
match (&scope.kind, required_kind) {
(Some(scope_kind), Some(req_kind)) => {
if scope_kind != req_kind {
@@ -672,14 +720,17 @@ mod tests {
#[test]
fn test_route_domain_extraction() {
let (domain, kind) = extract_domain_from_route("/api/w/test_workspace/jobs/123").unwrap();
let (domain, kind, route_suffix) =
extract_domain_from_route("/api/w/test_workspace/jobs/123").unwrap();
assert_eq!(domain, ScopeDomain::Jobs);
assert_eq!(kind, None);
assert_eq!(route_suffix, Some("jobs/123".to_string()));
let (domain, kind) =
let (domain, kind, route_suffix) =
extract_domain_from_route("/api/w/test_workspace/scripts/test_script").unwrap();
assert_eq!(domain, ScopeDomain::Scripts);
assert_eq!(kind, None);
assert_eq!(route_suffix, Some("scripts/test_script".to_string()));
}
#[test]

View File

@@ -933,6 +933,13 @@ async fn create_script_internal<'c>(
}
let permissioned_as = username_to_permissioned_as(&authed.username);
if let Some(parent_hash) = ns.parent_hash {
tracing::info!(
"creating script {hash:?} at path {script_path} with parent {parent_hash} on workspace {w_id}",
);
} else {
tracing::info!("creating script {hash:?} at path {script_path} on workspace {w_id}",);
}
if needs_lock_gen {
let tag = if ns.dedicated_worker.is_some_and(|x| x) {
Some(format!("{}:{}", &w_id, &ns.path,))
@@ -1490,13 +1497,23 @@ async fn get_script_by_hash_internal<'c>(
Ok(script)
}
#[derive(Deserialize)]
struct GetScriptByHashQuery {
authed: Option<bool>,
}
async fn get_script_by_hash(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, hash)): Path<(String, ScriptHash)>,
Query(query): Query<WithStarredInfoQuery>,
Query(query_auth): Query<GetScriptByHashQuery>,
Extension(authed): Extension<ApiAuthed>,
) -> JsonResult<ScriptWithStarred> {
let mut tx = db.begin().await?;
let mut tx = if query_auth.authed.is_some_and(|x| x) {
user_db.begin(&authed).await?
} else {
db.begin().await?
};
let r = get_script_by_hash_internal(
&mut tx,
&w_id,

View File

@@ -31,6 +31,7 @@ use crate::{
check_tag_available_for_workspace, delete_job_metadata_after_use, result_to_response,
run_flow_by_path_inner, run_script_by_path_inner, run_wait_result_internal, RunJobQuery,
},
utils::check_scopes,
HTTP_CLIENT,
};
@@ -703,13 +704,15 @@ async fn trigger_script_with_retry_and_error_handler(
error_handler_args: Option<&sqlx::types::Json<HashMap<String, Box<RawValue>>>>,
trigger_path: String,
) -> Result<(Uuid, Option<bool>)> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
check_scopes(&authed, || format!("jobs:run:scripts:{script_path}"))?;
let retry = retry.map(|r| r.0.clone());
let error_handler_path = error_handler_path.map(|p| p.to_string());
let error_handler_args = error_handler_args.map(|args| args.0.clone());
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
let (job_payload, tag, delete_after_use, timeout, on_behalf_of) = {
let mut tx = user_db.clone().begin(&authed).await?;
script_path_to_payload(script_path, &mut *tx, &workspace_id, Some(false)).await?

View File

@@ -199,17 +199,51 @@ pub async fn list_tokens_internal(
) -> JsonResult<Vec<TruncatedTokenWithEmail>> {
let tokens = if is_flow {
sqlx::query_as!(
TruncatedTokenWithEmail,
"SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
w_id, path).fetch_all(db)
TruncatedTokenWithEmail,
r#"
SELECT label,
concat(substring(token for 10)) AS token_prefix,
expiration,
created_at,
last_used_at,
scopes,
email
FROM token
WHERE workspace_id = $1
AND (
scopes @> ARRAY['jobs:run:flows:' || $2]::text[]
OR scopes @> ARRAY['run:flows/' || $2]::text[]
)
"#,
w_id,
path
)
.fetch_all(db)
.await?
} else {
sqlx::query_as!(
TruncatedTokenWithEmail,
"SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:script/' || $2]::text[]",
w_id, path)
r#"
SELECT label,
concat(substring(token for 10)) AS token_prefix,
expiration,
created_at,
last_used_at,
scopes,
email
FROM token
WHERE workspace_id = $1
AND (
scopes @> ARRAY['jobs:run:scripts:' || $2]::text[]
OR scopes @> ARRAY['run:scripts/' || $2]::text[]
)
"#,
w_id,
path
)
.fetch_all(db)
.await?
};
Ok(Json(tokens))
}

View File

@@ -8,7 +8,7 @@
use axum::{body::Body, response::Response};
use regex::Regex;
use serde::{Deserialize, Deserializer, Serialize};
use serde::{Deserialize, Deserializer};
use sqlx::{Postgres, Transaction};
#[cfg(feature = "enterprise")]
use windmill_common::worker::CLOUD_HOSTED;
@@ -224,7 +224,8 @@ where
Ok(o.filter(|s| !s.trim().is_empty()))
}
#[derive(Serialize)]
#[cfg(feature = "enterprise")]
#[derive(serde::Serialize)]
pub struct CriticalAlert {
id: i32,
alert_type: String,

View File

@@ -146,33 +146,12 @@ async fn get_custom_tags(Query(query): Query<CustomTagQuery>) -> JsonResult<Vec<
}
if let Some(workspace) = query.workspace {
let tags_o = CUSTOM_TAGS_PER_WORKSPACE.read().await;
let workspace_tags = tags_o
.1
.iter()
.filter(|(_, workspaces)| workspaces.contains(&workspace))
.map(|(tag, _)| tag.clone())
.collect::<Vec<String>>();
let all_tags = tags_o.0.clone();
return Ok(Json(
all_tags
.into_iter()
.chain(workspace_tags.into_iter())
.collect(),
));
let all_tags = tags_o.to_string_vec(Some(workspace));
return Ok(Json(all_tags));
} else if query.show_workspace_restriction.is_some_and(|x| x) {
let tags_o = CUSTOM_TAGS_PER_WORKSPACE.read().await;
let workspace_tags = tags_o
.1
.iter()
.map(|(tag, workspaces)| format!("{}({})", tag, workspaces.join("+")))
.collect::<Vec<String>>();
let all_tags = tags_o.0.clone();
return Ok(Json(
all_tags
.into_iter()
.chain(workspace_tags.into_iter())
.collect(),
));
let all_tags = tags_o.to_string_vec(None);
return Ok(Json(all_tags));
}
Ok(Json(ALL_TAGS.read().await.clone().into()))
}

View File

@@ -242,15 +242,6 @@ pub struct WorkspaceSettings {
pub git_app_installations: Option<serde_json::Value>,
}
#[derive(FromRow, Serialize, Debug)]
pub struct Usage {
pub workspace_id: String,
pub slack_team_id: Option<String>,
pub slack_name: Option<String>,
pub slack_command_script: Option<String>,
pub slack_email: String,
}
#[derive(sqlx::Type, Serialize, Deserialize, Debug)]
#[sqlx(type_name = "WORKSPACE_KEY_KIND", rename_all = "lowercase")]
pub enum WorkspaceKeyKind {

View File

@@ -17,7 +17,7 @@ pub enum ActionKind {
#[derive(FromRow, Serialize, Deserialize)]
pub struct AuditLog {
pub workspace_id: String,
pub id: i32,
pub id: i64,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub username: String,
pub operation: String,

View File

@@ -63,6 +63,7 @@ object_store = { workspace = true, optional = true }
prometheus = { workspace = true, optional = true }
aws-config = { workspace = true, optional = true }
aws-sdk-sts = { workspace = true, optional = true }
aws-smithy-types-convert = { workspace = true, optional = true }
indexmap.workspace = true
bytes.workspace = true

View File

@@ -323,8 +323,6 @@ pub async fn create_token_for_owner(
#[cfg(feature = "aws_auth")]
pub mod aws {
use crate::error::to_anyhow;
use super::*;
use crate::utils::empty_as_none;
use aws_config::{BehaviorVersion, Region};
@@ -332,7 +330,9 @@ pub mod aws {
config::Credentials as AwsCredentials,
operation::{
assume_role_with_saml::AssumeRoleWithSamlOutput,
assume_role_with_web_identity::AssumeRoleWithWebIdentityOutput,
assume_role_with_web_identity::{
builders::AssumeRoleWithWebIdentityFluentBuilder, AssumeRoleWithWebIdentityOutput,
},
},
types::Credentials,
Client,
@@ -396,33 +396,27 @@ pub mod aws {
Oidc(OidcAuth),
}
pub async fn get_oidc_authentication_data(
oidc_auth: OidcAuth,
role_session_name: Option<impl ToString>,
pub async fn get_assume_role_with_web_identity_fluent_builder(
oidc_auth: &OidcAuth,
token: String,
) -> Result<AssumeRoleWithWebIdentityOutput> {
let region = oidc_auth.region.unwrap_or_else(|| "us-east-1".to_string());
role_session_name: Option<impl ToString>,
) -> Result<AssumeRoleWithWebIdentityFluentBuilder> {
let region = oidc_auth.region.as_deref().unwrap_or_else(|| "us-east-1");
let credentials = AwsCredentials::new("", "", None, None, "UserInput");
let config = aws_config::defaults(BehaviorVersion::latest())
.credentials_provider(credentials)
.region(Region::new(region.clone()))
.region(Region::new(region.to_string()))
.load()
.await;
let assume_role_with_web_identity_fluent_builder = Client::new(&config)
.assume_role_with_web_identity()
.set_role_arn(Some(oidc_auth.role_arn))
.set_role_arn(Some(oidc_auth.role_arn.to_owned()))
.set_role_session_name(role_session_name.map(|str| str.to_string()))
.set_web_identity_token(Some(token));
let resp = assume_role_with_web_identity_fluent_builder
.clone()
.send()
.await
.map_err(to_anyhow)?;
Ok(resp)
Ok(assume_role_with_web_identity_fluent_builder)
}
}

View File

@@ -40,6 +40,8 @@ pub enum Error {
RequireAdmin(String),
#[error("{0}")]
ExecutionErr(String),
#[error("{0}")]
ResultTooLarge(String),
#[error("IoErr: {error:#} @{location:#}")]
IoErr { error: io::Error, location: String },
#[error("Utf8Err: {error:#} @{location:#}")]
@@ -84,6 +86,44 @@ pub enum Error {
Generic(StatusCode, String),
}
impl Error {
pub fn name(&self) -> &str {
match self {
Self::ExecutionErr(_) => "ExecutionErr",
Self::ResultTooLarge(_) => "ResultTooLarge",
Self::BadRequest(_) => "BadRequest",
Self::QuotaExceeded(_) => "QuotaExceeded",
Self::InternalErr(_) => "InternalErr",
Self::InternalErrLoc { .. } => "InternalErr",
Self::InternalErrAt(_, _) => "InternalErr",
Self::Anyhow { .. } => "Anyhow",
Self::JsonErr(_) => "JsonErr",
Self::AIError(_) => "AIError",
Self::AlreadyCompleted(_) => "AlreadyCompleted",
Self::FindPythonError(_) => "FindPythonError",
Self::ArgumentErr(_) => "ArgumentErr",
Self::Generic(_, _) => "Generic",
Self::IoErr { .. } => "IoErr",
Self::Utf8Err { .. } => "Utf8Err",
Self::UuidErr { .. } => "UuidErr",
Self::SqlErr { .. } => "SqlErr",
Self::SerdeJson { .. } => "SerdeJson",
Self::HexErr { .. } => "HexErr",
Self::DatabaseMigration(_) => "DatabaseMigration",
Self::ExitStatus(_, _) => "ExitStatus",
Self::ExecutionRawError(_) => "ExecutionRawError",
Self::BadGateway(_) => "BadGateway",
Self::BadConfig(_) => "BadConfig",
Self::ConnectingToDatabase(_) => "ConnectingToDatabase",
Self::NotFound(_) => "NotFound",
Self::NotAuthorized(_) => "NotAuthorized",
Self::MetricNotFound(_) => "MetricNotFound",
Self::PermissionDenied(_) => "PermissionDenied",
_ => "InternalErr",
}
}
}
fn prettify_location(location: &'static Location<'static>) -> String {
location
.to_string()

View File

@@ -97,6 +97,8 @@ pub struct QueuedJob {
pub permissioned_as: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub flow_status: Option<Json<Box<RawValue>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workflow_as_code_status: Option<Json<Box<RawValue>>>,
pub is_flow_step: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<ScriptLang>,
@@ -179,6 +181,7 @@ impl Default for QueuedJob {
job_kind: JobKind::Identity,
schedule_path: None,
permissioned_as: "".to_string(),
workflow_as_code_status: None,
flow_status: None,
is_flow_step: false,
language: None,
@@ -236,6 +239,8 @@ pub struct CompletedJob {
pub permissioned_as: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub flow_status: Option<sqlx::types::Json<Box<RawValue>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workflow_as_code_status: Option<sqlx::types::Json<Box<RawValue>>>,
pub is_flow_step: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<ScriptLang>,
@@ -658,16 +663,10 @@ pub async fn check_tag_available_for_workspace_internal(
}
let custom_tags_per_w = CUSTOM_TAGS_PER_WORKSPACE.read().await;
if custom_tags_per_w.0.contains(&tag.to_string()) {
is_tag_in_workspace_custom_tags = true;
} else if custom_tags_per_w.1.contains_key(tag)
&& custom_tags_per_w
.1
.get(tag)
.unwrap()
.contains(&w_id.to_string())
{
if custom_tags_per_w.global.contains(&tag.to_string()) {
is_tag_in_workspace_custom_tags = true;
} else if let Some(specific_tag) = custom_tags_per_w.specific.get(tag) {
is_tag_in_workspace_custom_tags = specific_tag.applies_to_workspace(w_id);
}
match is_tag_in_scope_tags {

View File

@@ -82,6 +82,7 @@ pub mod variables;
pub mod worker;
pub mod workspaces;
pub mod triggers;
pub mod stream;
pub const DEFAULT_MAX_CONNECTIONS_SERVER: u32 = 50;
pub const DEFAULT_MAX_CONNECTIONS_WORKER: u32 = 5;

View File

@@ -6,7 +6,7 @@ pub use crate::stats_ee::*;
use sqlx::Postgres;
#[cfg(not(feature = "private"))]
use crate::{error::Result, scripts::ScriptLang, DB};
use crate::{error::Result, DB};
#[cfg(not(feature = "private"))]
pub async fn get_disable_stats_setting(_db: &DB) -> bool {
@@ -20,14 +20,6 @@ pub async fn schedule_stats(_db: &DB, _http_client: &reqwest::Client) -> () {
// stats details are closed source
}
#[cfg(not(feature = "private"))]
#[derive(Debug, sqlx::FromRow, serde::Serialize)]
struct JobsUsage {
language: Option<ScriptLang>,
total_duration: i64,
count: i64,
}
#[cfg(not(feature = "private"))]
pub enum SendStatsReason {
Manual,

View File

@@ -0,0 +1,42 @@
use futures::{Stream, TryStreamExt};
pub async fn collect_stream_with_limits<T, S>(
mut row_stream: S,
max_size_megabytes: usize,
estimate_row_size: fn(&T) -> anyhow::Result<usize>,
cutoff_time: chrono::TimeDelta,
) -> Result<Vec<T>, anyhow::Error>
where
S: Stream<Item = Result<T, sqlx::Error>> + Unpin,
{
let started_time = chrono::Utc::now();
let mut results = Vec::new();
let mut total_size = 0;
let max_size_bytes = max_size_megabytes * 1024 * 1024;
while let Some(row) = row_stream.try_next().await? {
let row_size = estimate_row_size(&row)?;
results.push(row);
total_size += row_size;
if total_size + row_size > max_size_bytes {
tracing::info!(
"Stream was cutoff at {} elements because the collected size reached the treshold of {} MB",
results.len(),
max_size_megabytes
);
break;
}
if chrono::Utc::now() - started_time > cutoff_time {
tracing::info!(
"Stream was cutoff at {} elements because collecting it was taking longer than {}s",
results.len(),
cutoff_time.num_seconds()
);
break;
}
}
Ok(results)
}

View File

@@ -198,20 +198,13 @@ fn instance_name(hostname: &str) -> String {
const DEFAULT_WORKER_SUFFIX_LEN: usize = 5;
pub const SSH_AGENT_WORKER_SUFFIX: &'static str = "/ssh";
pub fn create_worker_suffix(hostname: &str, rd_string_len: usize, ssh_ag_worker: bool) -> String {
let mut wk_suffix = format!("{}-{}", instance_name(hostname), rd_string(rd_string_len));
if ssh_ag_worker {
wk_suffix.push_str(SSH_AGENT_WORKER_SUFFIX);
}
pub fn create_worker_suffix(hostname: &str, rd_string_len: usize) -> String {
let wk_suffix = format!("{}-{}", instance_name(hostname), rd_string(rd_string_len));
wk_suffix
}
pub fn create_ssh_agent_worker_suffix(hostname: &str) -> String {
create_worker_suffix(hostname, DEFAULT_WORKER_SUFFIX_LEN, true)
}
pub fn create_default_worker_suffix(hostname: &str) -> String {
create_worker_suffix(hostname, DEFAULT_WORKER_SUFFIX_LEN, false)
create_worker_suffix(hostname, DEFAULT_WORKER_SUFFIX_LEN)
}
pub fn worker_name_with_suffix(is_agent: bool, worker_group: &str, suffix: &str) -> String {
@@ -222,6 +215,14 @@ pub fn worker_name_with_suffix(is_agent: bool, worker_group: &str, suffix: &str)
}
}
pub fn retrieve_common_worker_prefix(worker_name: &str) -> String {
let (prefix, _) = worker_name.rsplit_once('-').unzip();
prefix
.expect("Invalid worker_name: expected at least one '-' in the name")
.to_owned()
}
pub fn paginate(pagination: Pagination) -> (usize, usize) {
let per_page = pagination
.per_page

View File

@@ -35,8 +35,105 @@ use crate::{
KillpillSender, DB,
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct CustomTags {
pub global: Vec<String>,
pub specific: HashMap<String, SpecificTagData>,
}
impl CustomTags {
pub fn from(tags: Vec<String>) -> Self {
let mut global = vec![];
let mut specific: HashMap<String, SpecificTagData> = HashMap::new();
for e in tags {
if let Some(cap) = CUSTOM_TAG_REGEX.captures(&e) {
let tag_name = cap.get(1).unwrap().as_str().to_string();
let workspace_str = cap.get(2).unwrap().as_str();
let tag_type = SpecificTagType::from_regex_string(workspace_str);
let workspaces: Vec<String> = workspace_str
.split(tag_type.corresponding_separator())
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect();
if workspaces.is_empty() {
tracing::warn!("Ignoring tag `{}` with empty exclusion/inclusion list", e);
global.push(e);
continue;
}
specific.insert(tag_name, SpecificTagData { tag_type, workspaces });
} else {
global.push(e.to_string());
}
}
Self { global, specific }
}
pub fn to_string_vec(&self, filter_with_workspace: Option<String>) -> Vec<String> {
let specific = if let Some(workspace) = filter_with_workspace {
self.specific
.iter()
.filter(|(_, tag_data)| tag_data.applies_to_workspace(&workspace))
.map(|(tag, _)| tag.clone())
.collect::<Vec<String>>()
} else {
self.specific
.iter()
.map(|(tag, tag_data)| {
let separator = tag_data.tag_type.corresponding_separator();
let mut workspaces = tag_data.workspaces.join(&*separator.to_string());
if tag_data.tag_type == SpecificTagType::AllExcluding {
// the AllExcluding tag syntax has a leading separator
workspaces.insert(0, separator);
}
format!("{}({})", tag, workspaces)
})
.collect::<Vec<String>>()
};
let all_tags = self.global.clone();
all_tags.into_iter().chain(specific.into_iter()).collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SpecificTagData {
pub tag_type: SpecificTagType,
pub workspaces: Vec<String>,
}
impl SpecificTagData {
pub fn applies_to_workspace(&self, workspace_id: &str) -> bool {
match self.tag_type {
SpecificTagType::AllExcluding => !self.workspaces.contains(&workspace_id.to_string()),
SpecificTagType::NoneExcept => self.workspaces.contains(&workspace_id.to_string()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum SpecificTagType {
AllExcluding,
NoneExcept,
}
impl SpecificTagType {
pub fn corresponding_separator(&self) -> char {
match self {
SpecificTagType::AllExcluding => '^',
SpecificTagType::NoneExcept => '+',
}
}
pub fn from_regex_string(workspaces_str: &str) -> Self {
if workspaces_str.contains(SpecificTagType::AllExcluding.corresponding_separator()) {
SpecificTagType::AllExcluding
} else {
// Regex match ensures the second branch is correct
SpecificTagType::NoneExcept
}
}
}
pub const DEFAULT_CLOUD_TIMEOUT: u64 = 900;
pub const DEFAULT_SELFHOSTED_TIMEOUT: u64 = 604800; // 7 days
pub const MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS: u64 = 60;
lazy_static::lazy_static! {
pub static ref WORKER_GROUP: String = std::env::var("WORKER_GROUP").unwrap_or_else(|_| {
#[cfg(not(feature = "enterprise"))]
@@ -107,6 +204,8 @@ lazy_static::lazy_static! {
dedicated_worker: Default::default(),
cache_clear: Default::default(),
init_bash: Default::default(),
periodic_script_bash: Default::default(),
periodic_script_interval_seconds: Default::default(),
additional_python_paths: Default::default(),
pip_local_dependencies: Default::default(),
env_vars: Default::default(),
@@ -127,11 +226,21 @@ lazy_static::lazy_static! {
.map(|x| x.split(',').map(|x| x.to_string()).collect::<Vec<_>>()).unwrap_or_default();
pub static ref CUSTOM_TAGS_PER_WORKSPACE: Arc<RwLock<(Vec<String>, HashMap<String, Vec<String>>)>> = Arc::new(RwLock::new((vec![], HashMap::new())));
pub static ref CUSTOM_TAGS_PER_WORKSPACE: Arc<RwLock<CustomTags>> = Arc::new(RwLock::new(CustomTags::default()));
pub static ref ALL_TAGS: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(vec![]));
static ref CUSTOM_TAG_REGEX: Regex = Regex::new(r"^([\w-]+)\(((?:[\w-]+\+)*[\w-]+)\)$").unwrap();
// ^([\w-]+) # Group 1: tag name
// \( # Literal '('
// ( # Group 2: the full workspace list
// (?:[\w-]+\+)*[\w-]+ # NoneExcept pattern: ws1+ws2
// | # OR
// (?:\^[\w-]+)+ # AllExcluding pattern: ^ws1^ws2
// )
// \)$ # Closing ')'
static ref CUSTOM_TAG_REGEX: Regex = Regex::new(r"^([\w-]+)\(((?:[\w-]+\+)*[\w-]+|(?:\^[\w-]+)+)\)$").unwrap();
pub static ref DISABLE_BUNDLING: bool = std::env::var("DISABLE_BUNDLING")
.ok()
@@ -470,12 +579,12 @@ pub async fn reload_custom_tags_setting(db: &DB) -> error::Result<()> {
CUSTOM_TAGS.clone()
};
let custom_tags = process_custom_tags(tags);
let custom_tags = CustomTags::from(tags);
tracing::info!(
"Loaded setting custom_tags, common: {:?}, per-workspace: {:?}",
custom_tags.0,
custom_tags.1,
custom_tags.global,
custom_tags.specific,
);
{
@@ -485,29 +594,18 @@ pub async fn reload_custom_tags_setting(db: &DB) -> error::Result<()> {
{
let mut l = ALL_TAGS.write().await;
*l = [
custom_tags.0.clone(),
custom_tags.1.keys().map(|x| x.to_string()).collect_vec(),
custom_tags.global.clone(),
custom_tags
.specific
.keys()
.map(|x| x.to_string())
.collect_vec(),
]
.concat();
}
Ok(())
}
fn process_custom_tags(tags: Vec<String>) -> (Vec<String>, HashMap<String, Vec<String>>) {
let mut global = vec![];
let mut specific: HashMap<String, Vec<String>> = HashMap::new();
for e in tags {
if let Some(cap) = CUSTOM_TAG_REGEX.captures(&e) {
let tag = cap.get(1).unwrap().as_str().to_string();
let workspaces = cap.get(2).unwrap().as_str().split("+");
specific.insert(tag, workspaces.map(|x| x.to_string()).collect_vec());
} else {
global.push(e.to_string());
}
}
(global, specific)
}
fn parse_file<T: FromStr>(path: &str) -> Option<T> {
std::process::Command::new("cat")
.args([path])
@@ -1433,6 +1531,34 @@ pub async fn load_worker_config(
&env_vars_static,
);
let periodic_script_bash = config
.periodic_script_bash
.or_else(|| load_periodic_bash_script_from_env())
.and_then(|x| if x.is_empty() { None } else { Some(x) });
let periodic_script_interval_seconds = config
.periodic_script_interval_seconds
.or_else(|| load_periodic_bash_script_interval_from_env());
if periodic_script_bash.is_some() {
if let Some(interval) = periodic_script_interval_seconds {
if interval < MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS {
killpill_tx.send();
return Err(anyhow::anyhow!(
"Periodic script interval must be at least {} seconds, got {} seconds",
MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS,
interval
)
.into());
}
} else {
killpill_tx.send();
return Err(anyhow::anyhow!(
"Periodic script interval must be specified when periodic script is configured"
)
.into());
}
}
Ok(WorkerConfig {
worker_tags,
priority_tags_sorted,
@@ -1441,6 +1567,8 @@ pub async fn load_worker_config(
.init_bash
.or_else(|| load_init_bash_from_env())
.and_then(|x| if x.is_empty() { None } else { Some(x) }),
periodic_script_bash,
periodic_script_interval_seconds,
cache_clear: config.cache_clear,
pip_local_dependencies: config
.pip_local_dependencies
@@ -1458,6 +1586,18 @@ pub fn load_init_bash_from_env() -> Option<String> {
.and_then(|x| if x.is_empty() { None } else { Some(x) })
}
pub fn load_periodic_bash_script_from_env() -> Option<String> {
std::env::var("PERIODIC_BASH_SCRIPT")
.ok()
.and_then(|x| if x.is_empty() { None } else { Some(x) })
}
pub fn load_periodic_bash_script_interval_from_env() -> Option<u64> {
std::env::var("PERIODIC_BASH_SCRIPT_INTERVAL_SECONDS")
.ok()
.and_then(|x| x.parse::<u64>().ok())
}
pub fn load_pip_local_dependencies_from_env() -> Option<Vec<String>> {
std::env::var("PIP_LOCAL_DEPENDENCIES")
.ok()
@@ -1510,6 +1650,8 @@ pub struct WorkerConfigOpt {
pub priority_tags: Option<HashMap<String, u8>>,
pub dedicated_worker: Option<String>,
pub init_bash: Option<String>,
pub periodic_script_bash: Option<String>,
pub periodic_script_interval_seconds: Option<u64>,
pub cache_clear: Option<u32>,
pub additional_python_paths: Option<Vec<String>>,
pub pip_local_dependencies: Option<Vec<String>>,
@@ -1524,6 +1666,8 @@ impl Default for WorkerConfigOpt {
priority_tags: Default::default(),
dedicated_worker: Default::default(),
init_bash: Default::default(),
periodic_script_bash: Default::default(),
periodic_script_interval_seconds: Default::default(),
cache_clear: Default::default(),
additional_python_paths: Default::default(),
pip_local_dependencies: Default::default(),
@@ -1539,6 +1683,8 @@ pub struct WorkerConfig {
pub priority_tags_sorted: Vec<PriorityTags>,
pub dedicated_worker: Option<WorkspacedPath>,
pub init_bash: Option<String>,
pub periodic_script_bash: Option<String>,
pub periodic_script_interval_seconds: Option<u64>,
pub cache_clear: Option<u32>,
pub additional_python_paths: Option<Vec<String>>,
pub pip_local_dependencies: Option<Vec<String>>,
@@ -1547,8 +1693,8 @@ pub struct WorkerConfig {
impl std::fmt::Debug for WorkerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "WorkerConfig {{ worker_tags: {:?}, priority_tags_sorted: {:?}, dedicated_worker: {:?}, init_bash: {:?}, cache_clear: {:?}, additional_python_paths: {:?}, pip_local_dependencies: {:?}, env_vars: {:?} }}",
self.worker_tags, self.priority_tags_sorted, self.dedicated_worker, self.init_bash, self.cache_clear, self.additional_python_paths, self.pip_local_dependencies, self.env_vars.iter().map(|(k, v)| format!("{}: {}{} ({} chars)", k, &v[..3.min(v.len())], "***", v.len())).collect::<Vec<String>>().join(", "))
write!(f, "WorkerConfig {{ worker_tags: {:?}, priority_tags_sorted: {:?}, dedicated_worker: {:?}, init_bash: {:?}, periodic_script_bash: {:?}, periodic_script_interval_seconds: {:?}, cache_clear: {:?}, additional_python_paths: {:?}, pip_local_dependencies: {:?}, env_vars: {:?} }}",
self.worker_tags, self.priority_tags_sorted, self.dedicated_worker, self.init_bash, self.periodic_script_bash, self.periodic_script_interval_seconds, self.cache_clear, self.additional_python_paths, self.pip_local_dependencies, self.env_vars.iter().map(|(k, v)| format!("{}: {}{} ({} chars)", k, &v[..3.min(v.len())], "***", v.len())).collect::<Vec<String>>().join(", "))
}
}
@@ -1567,3 +1713,138 @@ pub fn to_raw_value_owned(result: serde_json::Value) -> Box<RawValue> {
serde_json::value::to_raw_value(&result)
.unwrap_or_else(|_| RawValue::from_string("{}".to_string()).unwrap())
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_mixed_tags() {
let input = vec![
"global".to_string(),
"feat(ws1+ws2)".to_string(),
"hotfix(^ws3^ws4)".to_string(),
];
let result = CustomTags::from(input);
assert_eq!(result.global, vec!["global"]);
let mut expected = HashMap::new();
expected.insert(
"feat".to_string(),
SpecificTagData {
tag_type: SpecificTagType::NoneExcept,
workspaces: vec!["ws1".to_string(), "ws2".to_string()],
},
);
expected.insert(
"hotfix".to_string(),
SpecificTagData {
tag_type: SpecificTagType::AllExcluding,
workspaces: vec!["ws3".to_string(), "ws4".to_string()],
},
);
assert_eq!(result.specific, expected);
}
#[test]
fn test_invalid_specific_tag_format() {
let input = vec!["invalid(custom+format".to_string()];
let result = CustomTags::from(input);
// Regex does not match, so it's treated as a global tag.
assert_eq!(result.global, vec!["invalid(custom+format"]);
assert!(result.specific.is_empty());
}
#[test]
fn test_empty_input() {
let input: Vec<String> = vec![];
let result = CustomTags::from(input);
assert!(result.global.is_empty());
assert!(result.specific.is_empty());
}
#[test]
fn test_custom_tags_from_parses_global_tags_correctly() {
let input = vec!["frontend".to_string(), "urgent".to_string()];
let tags = CustomTags::from(input.clone());
assert_eq!(tags.global, input);
assert!(tags.specific.is_empty());
}
#[test]
fn test_custom_tags_from_parses_specific_tags_none_except() {
let input = vec!["urgent(ws1+ws2)".to_string()];
let tags = CustomTags::from(input.clone());
assert!(tags.global.is_empty());
assert_eq!(tags.specific.len(), 1);
let data = tags.specific.get("urgent").unwrap();
assert_eq!(data.tag_type, SpecificTagType::NoneExcept);
assert_eq!(data.workspaces, vec!["ws1", "ws2"]);
}
#[test]
fn test_custom_tags_from_parses_specific_tags_all_excluding() {
let input = vec!["legacy(^ws1^ws2)".to_string()];
let tags = CustomTags::from(input.clone());
assert!(tags.global.is_empty());
assert_eq!(tags.specific.len(), 1);
let data = tags.specific.get("legacy").unwrap();
assert_eq!(data.tag_type, SpecificTagType::AllExcluding);
assert_eq!(data.workspaces, vec!["ws1", "ws2"]);
}
#[test]
fn test_custom_tags_from_ignores_empty_workspace_list_as_global() {
let input = vec!["foo()".to_string(), "bar(^)".to_string()];
let tags = CustomTags::from(input.clone());
assert_eq!(tags.global, input);
assert!(tags.specific.is_empty());
}
#[test]
fn test_custom_tags_from_filters_specific_tags_by_workspace_none_except() {
let input = vec!["urgent(ws1+ws2)".to_string()];
let tags = CustomTags::from(input);
let output = tags.to_string_vec(Some("ws1".to_string()));
assert_eq!(output, vec!["urgent"]);
let output_none = tags.to_string_vec(Some("ws3".to_string()));
assert!(output_none.is_empty());
}
#[test]
fn test_custom_tags_from_filters_specific_tags_by_workspace_all_excluding() {
let input = vec!["legacy(^ws1^ws2)".to_string()];
let tags = CustomTags::from(input);
let output = tags.to_string_vec(Some("ws3".to_string()));
assert_eq!(output, vec!["legacy"]);
let output_excluded = tags.to_string_vec(Some("ws1".to_string()));
assert!(output_excluded.is_empty());
}
#[test]
fn test_custom_tags_from_reconstructs_all_tags_when_no_filter() {
let input = vec![
"foo".to_string(),
"urgent(ws1+ws2)".to_string(),
"legacy(^ws1^ws2)".to_string(),
];
let tags = CustomTags::from(input);
let mut result = tags.to_string_vec(None);
result.sort();
assert_eq!(result, vec!["foo", "legacy(^ws1^ws2)", "urgent(ws1+ws2)"]);
}
}

View File

@@ -8,8 +8,12 @@ edition.workspace = true
name = "windmill_indexer"
path = "src/lib.rs"
[[bin]]
name = "windmill_indexer_debug"
path = "./src/main.rs"
[features]
default = []
default = ["enterprise", "private", "parquet"]
parquet = ["dep:object_store"]
private = []
enterprise = []
@@ -32,3 +36,5 @@ object_store = { workspace = true, optional = true}
tokio-tar.workspace = true
lazy_static.workspace = true
const_format.workspace = true
flume.workspace = true
gethostname.workspace = true

View File

@@ -0,0 +1,131 @@
use anyhow::anyhow;
use const_format::concatcp;
use gethostname::gethostname;
use std::{str::FromStr, sync::{Arc, Mutex}};
use tantivy::{directory::MmapDirectory, schema::Schema};
use windmill_common::{
indexer::TantivyIndexerSettings, utils::{rd_string, Mode}, worker::TMP_DIR,
};
use windmill_indexer::{
completed_runs_ee::{
fill_schema, IndexWriter, IndexedJobTrackerData, INDEXED_JOB_TRACKER_FILENAME,
},
indexer_ee::{clean_dir_all_if_exists, IndexedDBTracker, S3BackedMMapDirectory},
};
lazy_static::lazy_static! {
static ref HOSTNAME :String = std::env::var("FORCE_HOSTNAME").unwrap_or_else(|_| {
gethostname()
.to_str()
.map(|x| x.to_string())
.unwrap_or_else(|| rd_string(5))
});
}
const S3_JOB_INDEX_PATH: &str = "search-dbg/completed_job_index";
const JOB_INDEX_DIRECTORY_PATH: &str = concatcp!(TMP_DIR, "/search-dbg/completed_jobs_index");
fn init_index() -> anyhow::Result<IndexWriter> {
let mut schema_builder = Schema::builder();
let fields = fill_schema(&mut schema_builder);
let schema = schema_builder.build();
std::fs::create_dir_all(JOB_INDEX_DIRECTORY_PATH)?;
let idx_tantivy_dir = MmapDirectory::open(JOB_INDEX_DIRECTORY_PATH)
.map_err(|e| anyhow!("Failed to create MMapDirectory: {e}"))?;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
let idx_tantivy_dir = S3BackedMMapDirectory::new(
std::path::PathBuf::from_str(JOB_INDEX_DIRECTORY_PATH).map_err(|e| anyhow!("{}", e))?,
idx_tantivy_dir,
S3_JOB_INDEX_PATH.to_string(),
tokio::runtime::Handle::current(),
);
clean_dir_all_if_exists(JOB_INDEX_DIRECTORY_PATH)?;
let index = tantivy::Index::open_or_create(idx_tantivy_dir, schema)
.map_err(|e| anyhow!("Failed to open index {e}"))?;
let job_tracker: IndexedDBTracker<IndexedJobTrackerData> = IndexedDBTracker::new(
std::path::PathBuf::from_str(INDEXED_JOB_TRACKER_FILENAME)
.map_err(|e| anyhow!("Failed to read {}: {e}", INDEXED_JOB_TRACKER_FILENAME))?,
index.directory().clone(),
);
Ok(IndexWriter { index, fields, job_tracker })
}
#[tokio::main]
pub async fn main() -> anyhow::Result<()> {
let hostname = HOSTNAME.to_owned();
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info")
}
let (_guard, _jaja) = windmill_common::tracing_init::initialize_tracing(&hostname, &Mode::Indexer, "");
let db = windmill_common::initial_connection().await?;
let mut idx_writer = init_index()?;
let max_created_at = std::env::var("INDEX_STARTING_FROM")
.ok()
.map(|start_from| chrono::DateTime::parse_from_rfc3339(&start_from))
.transpose()
.map_err(|e| anyhow!("Failed to parse date for INDEX_STARTING_FROM"))?;
if let Some(max_created_at) = max_created_at {
tracing::info!("Date to start indexing aka INDEX_STARTING_FROM is set to {:?}", max_created_at);
idx_writer.job_tracker.data =
Some(IndexedJobTrackerData { max_created_at: max_created_at.into(), queued_uuids_remainder: vec![] });
} else {
tracing::info!("INDEX_STARTING_FROM env var not found, indexing from the very first job");
}
let writer = idx_writer
.index
.writer(300 * 1024 * 1024)?;
let w = Arc::new(Mutex::new(writer));
idx_writer.refresh_jobs(w.clone(), &db, &TantivyIndexerSettings::default()).await?;
tokio::task::spawn_blocking(move || {
if let Err(e) = Arc::try_unwrap(w)
.map_err(|_| {
anyhow!(
"There was more than 1 refrence to the writer. This should not be possible."
)
})
.unwrap()
.into_inner()
.unwrap()
.wait_merging_threads()
{
tracing::error!("Error while waiting for index writer merging threads: {e}");
}
})
.await?;
tracing::info!("All merging threads completed, releasing lock");
Ok(())
}
// let (index_reader, index_writer) = {
// let mut indexer_rx = killpill_rx.resubscribe();
//
// let (mut reader, mut writer) = (None, None);
// tokio::select! {
// _ = indexer_rx.recv() => {
// tracing::info!("Received killpill, aborting index initialization");
// },
// res = windmill_indexer::completed_runs_oss::init_index(&db) => {
// let res = res?;
// reader = Some(res.0);
// writer = Some(res.1);
// }
//
// }
// (reader, writer)
// };

View File

@@ -20,8 +20,8 @@ use reqwest::Client;
use serde::Deserialize;
use serde::{ser::SerializeMap, Serialize};
use serde_json::{json, value::RawValue};
use sqlx::PgExecutor;
use sqlx::{types::Json, FromRow, Pool, Postgres, Transaction};
use sqlx::{Encode, PgExecutor};
use sqlx::{types::Json, Pool, Postgres, Transaction};
use tokio::{sync::RwLock, time::sleep};
use ulid::Ulid;
use uuid::Uuid;
@@ -401,6 +401,13 @@ pub async fn append_logs(
}
}
pub const PERIODIC_SCRIPT_TAG: &str = "periodic_bash_script";
pub const INIT_SCRIPT_TAG: &str = "init_script";
pub const INIT_SCRIPT_PATH_PREFIX: &str = "init_script_";
pub const PERIODIC_SCRIPT_PATH_PREFIX: &str = "periodic_script_";
pub async fn push_init_job<'c>(
db: &Pool<Postgres>,
content: String,
@@ -415,7 +422,7 @@ pub async fn push_init_job<'c>(
windmill_common::jobs::JobPayload::Code(windmill_common::jobs::RawCode {
hash: None,
content,
path: Some(format!("init_script_{worker_name}")),
path: Some(format!("{INIT_SCRIPT_PATH_PREFIX}{worker_name}")),
language: ScriptLang::Bash,
lock: None,
custom_concurrency_key: None,
@@ -438,7 +445,56 @@ pub async fn push_init_job<'c>(
true,
None,
true,
Some("init_script".to_string()),
Some(INIT_SCRIPT_TAG.to_string()),
None,
None,
None,
None,
)
.await?;
inner_tx.commit().await?;
Ok(uuid)
}
pub async fn push_periodic_bash_job<'c>(
db: &Pool<Postgres>,
content: String,
worker_name: &str,
) -> error::Result<Uuid> {
let tx = PushIsolationLevel::IsolatedRoot(db.clone());
let ehm = HashMap::new();
let timestamp = chrono::Utc::now().timestamp();
let (uuid, inner_tx) = push(
&db,
tx,
"admins",
windmill_common::jobs::JobPayload::Code(windmill_common::jobs::RawCode {
hash: None,
content,
path: Some(format!("{PERIODIC_SCRIPT_PATH_PREFIX}{}_{}", worker_name, timestamp)),
language: ScriptLang::Bash,
lock: None,
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None,
dedicated_worker: None,
}),
PushArgs::from(&ehm),
worker_name,
"worker@windmill.dev",
SUPERADMIN_SECRET_EMAIL.to_string(),
Some("worker_periodic_script_job"),
None,
None,
None,
None,
None,
false,
true,
None,
true,
Some(PERIODIC_SCRIPT_TAG.to_string()),
None,
None,
None,
@@ -517,6 +573,7 @@ pub struct WrappedError {
pub trait ValidableJson {
fn is_valid_json(&self) -> bool;
fn wm_labels(&self) -> Option<Vec<String>>;
fn size(&self) -> usize;
}
#[derive(serde::Deserialize)]
@@ -532,6 +589,10 @@ impl ValidableJson for WrappedError {
fn wm_labels(&self) -> Option<Vec<String>> {
None
}
fn size(&self) -> usize {
0
}
}
impl ValidableJson for Box<RawValue> {
@@ -544,6 +605,10 @@ impl ValidableJson for Box<RawValue> {
.ok()
.map(|r| r.wm_labels)
}
fn size(&self) -> usize {
self.get().len()
}
}
impl<T: ValidableJson> ValidableJson for Arc<T> {
@@ -554,6 +619,10 @@ impl<T: ValidableJson> ValidableJson for Arc<T> {
fn wm_labels(&self) -> Option<Vec<String>> {
T::wm_labels(&self)
}
fn size(&self) -> usize {
T::size(&self)
}
}
impl ValidableJson for serde_json::Value {
@@ -566,6 +635,10 @@ impl ValidableJson for serde_json::Value {
.ok()
.map(|r| r.wm_labels)
}
fn size(&self) -> usize {
self.size_hint()
}
}
impl<T: ValidableJson> ValidableJson for Json<T> {
@@ -576,6 +649,10 @@ impl<T: ValidableJson> ValidableJson for Json<T> {
fn wm_labels(&self) -> Option<Vec<String>> {
self.0.wm_labels()
}
fn size(&self) -> usize {
self.0.size()
}
}
pub async fn register_metric<T, F, F2, R>(
@@ -662,6 +739,7 @@ pub async fn add_completed_job_error(
lazy_static::lazy_static! {
pub static ref GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE: Option<String> = std::env::var("GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE").ok();
pub static ref MAX_RESULT_SIZE_MB: usize = std::env::var("MAX_RESULT_SIZE_MB").unwrap_or("500".to_string()).parse().unwrap_or(500);
}
pub async fn add_completed_job<T: Serialize + Send + Sync + ValidableJson>(
@@ -697,15 +775,34 @@ pub async fn add_completed_job<T: Serialize + Send + Sync + ValidableJson>(
let job_id = queued_job.id;
// tracing::error!("1 {:?}", start.elapsed());
tracing::debug!(
"completed job {} {}",
queued_job.id,
serde_json::to_string(&result).unwrap_or_else(|_| "".to_string())
);
// tracing::debug!(
// "completed job {} {}",
// queued_job.id,
// serde_json::to_string(&result).unwrap_or_else(|_| "".to_string())
// );
let mem_peak = mem_peak;
// add_time!(bench, "add_completed_job query START");
let result_size = result.size() / 1024 / 1024;
if result_size > 2 {
if result_size > *MAX_RESULT_SIZE_MB {
tracing::error!("Result of job {} is too large: {}MB > MAX_RESULT_SIZE_MB={}MB", queued_job.id, result_size, *MAX_RESULT_SIZE_MB);
return Err(Error::ResultTooLarge(format!("Result of job {} is too large: {}MB > MAX_RESULT_SIZE_MB={}MB.\nUse external storages such as the Windmill Object Storage to store large results: https://www.windmill.dev/docs/core_concepts/object_storage_in_windmill", queued_job.id, result_size, *MAX_RESULT_SIZE_MB)));
}
append_logs(
&queued_job.id,
&queued_job.workspace_id,
format!("Warning: Result of job {} is large: {}MB.\nRecommended max size is 2MB.\nPrefer using external storages such as the Windmill Object Storage to store large results: https://www.windmill.dev/docs/core_concepts/object_storage_in_windmill", queued_job.id, result_size),
&db.into(),
)
.await;
if *CLOUD_HOSTED {
return Err(Error::ResultTooLarge(format!("Result of job {} is too large for multi-tenant cloud: {}MB (max 2MB).\nUse external storages such as the Windmill Object Storage to store large results: https://www.windmill.dev/docs/core_concepts/object_storage_in_windmill", queued_job.id, result_size)));
} else {
tracing::warn!("Result of job {} is larger than 2MB: {}MB. Not recommended.", queued_job.id, result_size);
}
}
let _duration = sqlx::query_scalar!(
"INSERT INTO v2_job_completed AS cj
( workspace_id
@@ -996,7 +1093,7 @@ pub async fn add_completed_job<T: Serialize + Send + Sync + ValidableJson>(
.with_max_times(5)
.build(),
)
.when(|err| !matches!(err, Error::QuotaExceeded(_)))
.when(|err| !matches!(err, Error::QuotaExceeded(_)) && !matches!(err, Error::ResultTooLarge(_)))
.notify(|err, dur| {
tracing::error!(
"Could not insert completed job, retrying in {dur:#?}, err: {err:#?}"
@@ -1487,12 +1584,6 @@ pub async fn handle_maybe_scheduled_job<'c>(
}
}
#[derive(Clone, Serialize, FromRow)]
struct CompletedJobSubset {
success: bool,
result: Option<sqlx::types::Json<Box<RawValue>>>,
started_at: chrono::DateTime<chrono::Utc>,
}
#[cfg(feature = "enterprise")]
async fn apply_schedule_handlers<'a, 'c, T: Serialize + Send + Sync>(
@@ -3145,7 +3236,7 @@ async fn get_queued_job_tx<'c>(
tx: &mut Transaction<'c, Postgres>,
) -> error::Result<Option<QueuedJob>> {
sqlx::query_as::<_, QueuedJob>(
"SELECT *
"SELECT *, null as workflow_as_code_status
FROM v2_as_queue WHERE id = $1 AND workspace_id = $2",
)
.bind(id)
@@ -3157,7 +3248,7 @@ async fn get_queued_job_tx<'c>(
pub async fn get_queued_job(id: &Uuid, w_id: &str, db: &DB) -> error::Result<Option<QueuedJob>> {
sqlx::query_as::<_, QueuedJob>(
"SELECT *
"SELECT *, null as workflow_as_code_status
FROM v2_as_queue WHERE id = $1 AND workspace_id = $2",
)
.bind(id)
@@ -4694,7 +4785,7 @@ async fn restarted_flows_resolution(
}
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct SameWorkerPayload {
pub job_id: Uuid,
pub recoverable: bool,

View File

@@ -14,6 +14,17 @@ pub async fn queue_init_job(client: &HttpClient, content: &str) -> anyhow::Resul
.and_then(|x: String| Uuid::parse_str(&x).map_err(|e| anyhow::anyhow!(e)))
}
pub async fn queue_periodic_job(client: &HttpClient, content: &str) -> anyhow::Result<Uuid> {
client
.post(
"/api/agent_workers/queue_periodic_job",
None,
&QueueInitJob { content: content.to_string() },
)
.await
.and_then(|x: String| Uuid::parse_str(&x).map_err(|e| anyhow::anyhow!(e)))
}
pub async fn pull_job(
client: &HttpClient,
headers: Option<HeaderMap>,

View File

@@ -21,7 +21,9 @@ use windmill_common::{
#[cfg(feature = "dind")]
use windmill_common::error::to_anyhow;
use windmill_queue::{append_logs, CanceledBy, MiniPulledJob};
use windmill_queue::{
append_logs, CanceledBy, MiniPulledJob, INIT_SCRIPT_PATH_PREFIX, PERIODIC_SCRIPT_PATH_PREFIX,
};
lazy_static::lazy_static! {
pub static ref BIN_BASH: String = std::env::var("BASH_PATH").unwrap_or_else(|_| "/bin/bash".to_string());
@@ -163,7 +165,10 @@ exit $exit_status
&& job
.runnable_path
.as_ref()
.map(|x| !x.starts_with("init_script_"))
.map(|x| {
!x.starts_with(INIT_SCRIPT_PATH_PREFIX)
&& !x.starts_with(PERIODIC_SCRIPT_PATH_PREFIX)
})
.unwrap_or(true);
let child = if nsjail {
let _ = write_file(
@@ -367,6 +372,7 @@ async fn handle_docker_job(
.await;
loop {
tokio::select! {
biased;
log = log_stream.next() => {
match log {
Some(Ok(log)) => {
@@ -461,21 +467,22 @@ async fn handle_docker_job(
rm_container(&client, &container_id).await;
return Err(e);
} else {
let _ = tokio::time::timeout(tokio::time::Duration::from_secs(5), logs).await;
rm_container(&client, &container_id).await;
let result = result.unwrap();
if result.is_some_and(|x| x > 0) {
return Err(Error::ExecutionErr(format!(
"Docker job completed with unsuccessful exit status: {}",
result.unwrap()
)));
}
return Ok(to_raw_value(&json!(format!(
"Docker job completed with success exit status"
))));
}
rm_container(&client, &container_id).await;
let result = result.unwrap();
if result.is_some_and(|x| x > 0) {
return Err(Error::ExecutionErr(format!(
"Docker job completed with unsuccessful exit status: {}",
result.unwrap()
)));
}
return Ok(to_raw_value(&json!(format!(
"Docker job completed with success exit status"
))));
}
#[cfg(feature = "dind")]
@@ -733,7 +740,16 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
let _ = write_file(job_dir, "result.out", "")?;
let _ = write_file(job_dir, "result2.out", "")?;
let child = if !*DISABLE_NSJAIL {
let nsjail = !*DISABLE_NSJAIL
&& job
.runnable_path
.as_ref()
.map(|x| {
!x.starts_with(INIT_SCRIPT_PATH_PREFIX)
&& !x.starts_with(PERIODIC_SCRIPT_PATH_PREFIX)
})
.unwrap_or(true);
let child = if nsjail {
let _ = write_file(
job_dir,
"run.config.proto",
@@ -797,6 +813,7 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
#[cfg(windows)]
{
cmd.env("SystemRoot", SYSTEM_ROOT.as_str())
.env("WINDIR", SYSTEM_ROOT.as_str())
.env(
"LOCALAPPDATA",
std::env::var("LOCALAPPDATA")

View File

@@ -524,7 +524,7 @@ pub async fn update_worker_ping_for_failed_init_script(
pub fn error_to_value(err: Error) -> serde_json::Value {
match err {
Error::JsonErr(err) => err,
_ => json!({"message": err.to_string(), "name": "InternalErr"}),
_ => json!({"message": err.to_string(), "name": err.name()}),
}
}

View File

@@ -149,7 +149,6 @@ pub async fn do_duckdb(
.clone()
.unwrap_or_else(|| "text".to_string())
.as_str(),
client,
)?;
m.insert(sig_arg.name, duckdb_value);
}
@@ -314,7 +313,6 @@ fn row_to_value(row: &Row<'_>, column_names: &[String]) -> Result<Box<RawValue>>
fn json_value_to_duckdb_value(
json_value: &serde_json::Value,
arg_type: &str,
client: &AuthedClient,
) -> Result<duckdb::types::Value> {
let arg_type = arg_type.to_lowercase();
let duckdb_value = match json_value {
@@ -372,22 +370,12 @@ fn json_value_to_duckdb_value(
}
}
serde_json::Value::Array(arr) => duckdb::types::Value::Array(
arr.iter()
.map(|val| json_value_to_duckdb_value(val, arg_type.as_str(), client))
.collect::<Result<Vec<_>>>()?,
),
serde_json::Value::Object(map) => duckdb::types::Value::Struct(
map.iter()
.map(|(k, v)| {
Ok::<_, Error>((
k.clone(),
json_value_to_duckdb_value(v, arg_type.as_str(), client)?,
))
})
.collect::<Result<Vec<_>>>()?
.into(),
),
serde_json::Value::Array(arr) => {
duckdb::types::Value::Text(serde_json::to_string(arr).map_err(to_anyhow)?)
}
serde_json::Value::Object(map) => {
duckdb::types::Value::Text(serde_json::to_string(map).map_err(to_anyhow)?)
}
value @ _ => {
return Err(Error::ExecutionErr(format!(

View File

@@ -1,14 +1,14 @@
#[cfg(feature = "otel")]
use opentelemetry::trace::FutureExt;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use sqlx::types::Json;
use std::{
collections::HashMap,
sync::{
atomic::{AtomicBool, AtomicU16, Ordering},
Arc,
}
},
};
use tracing::{field, Instrument};
#[cfg(not(feature = "otel"))]
@@ -17,7 +17,12 @@ use windmill_common::otel_oss::FutureExt;
use uuid::Uuid;
use windmill_common::{
add_time, error::{self, Error}, jobs::JobKind, utils::WarnAfterExt, worker::{to_raw_value, Connection, WORKER_GROUP}, KillpillSender, DB
add_time,
error::{self, Error},
jobs::JobKind,
utils::WarnAfterExt,
worker::{to_raw_value, Connection, WORKER_GROUP},
KillpillSender, DB,
};
#[cfg(feature = "benchmark")]
@@ -25,6 +30,7 @@ use windmill_common::bench::{BenchmarkInfo, BenchmarkIter};
use windmill_queue::{
append_logs, get_queued_job, CanceledBy, JobCompleted, MiniPulledJob, WrappedError,
INIT_SCRIPT_TAG,
};
use serde_json::{json, value::RawValue, Value};
@@ -34,10 +40,21 @@ use tokio::{sync::Notify, task::JoinHandle};
use windmill_queue::{add_completed_job, add_completed_job_error};
use crate::{
bash_executor::ANSI_ESCAPE_RE, common::{error_to_value, read_result, save_in_cache}, otel_oss::add_root_flow_job_to_otlp, worker_flow::update_flow_status_after_job_completion, JobCompletedReceiver, JobCompletedSender, SameWorkerSender, SendResult, SendResultPayload, UpdateFlow, INIT_SCRIPT_TAG, SAME_WORKER_REQUIREMENTS
bash_executor::ANSI_ESCAPE_RE,
common::{error_to_value, read_result, save_in_cache},
otel_oss::add_root_flow_job_to_otlp,
worker_flow::update_flow_status_after_job_completion,
JobCompletedReceiver, JobCompletedSender, SameWorkerSender, SendResult, SendResultPayload,
UpdateFlow, SAME_WORKER_REQUIREMENTS,
};
use windmill_common::client::AuthedClient;
#[derive(Debug, Deserialize)]
struct ErrorMessage {
message: String,
name: String,
}
async fn process_jc(
jc: JobCompleted,
worker_name: &str,
@@ -50,17 +67,35 @@ async fn process_jc(
) {
let success: bool = jc.success;
let span = tracing::span!(
tracing::Level::INFO,
"job_postprocessing",
job_id = %jc.job.id, root_job = field::Empty, workspace_id = %jc.job.workspace_id, worker = %worker_name,tag = %jc.job.tag,
// hostname = %hostname,
language = field::Empty,
script_path = field::Empty,
flow_step_id = field::Empty,
parent_job = field::Empty,
otel.name = field::Empty
);
let span = if success {
tracing::span!(
tracing::Level::INFO,
"job_postprocessing",
job_id = %jc.job.id, root_job = field::Empty, workspace_id = %jc.job.workspace_id, worker = %worker_name,tag = %jc.job.tag,
// hostname = %hostname,
language = field::Empty,
script_path = field::Empty,
flow_step_id = field::Empty,
parent_job = field::Empty,
otel.name = field::Empty,
success = %success,
)
} else {
tracing::span!(
tracing::Level::INFO,
"job_postprocessing",
job_id = %jc.job.id, root_job = field::Empty, workspace_id = %jc.job.workspace_id, worker = %worker_name,tag = %jc.job.tag,
// hostname = %hostname,
language = field::Empty,
script_path = field::Empty,
flow_step_id = field::Empty,
parent_job = field::Empty,
otel.name = field::Empty,
success = %success,
error.message = field::Empty,
error.name = field::Empty,
)
};
let rj = if let Some(root_job) = jc.job.flow_innermost_root_job {
root_job
} else {
@@ -89,6 +124,12 @@ async fn process_jc(
if let Some(root_job) = jc.job.flow_innermost_root_job.as_ref() {
span.record("root_job", root_job.to_string().as_str());
}
if !success {
if let Ok(result_error) = serde_json::from_str::<ErrorMessage>(jc.result.get()) {
span.record("error.message", result_error.message.as_str());
span.record("error.name", result_error.name.as_str());
}
}
let root_job = handle_receive_completed_job(
jc,
@@ -173,7 +214,10 @@ pub fn start_background_processor(
let mut bench = BenchmarkIter::new();
match sr {
JobCompletedRx::JobCompleted(SendResult { result: SendResultPayload::JobCompleted(jc), time }) => {
JobCompletedRx::JobCompleted(SendResult {
result: SendResultPayload::JobCompleted(jc),
time,
}) => {
let is_init_script_and_failure =
!jc.success && jc.job.tag.as_str() == INIT_SCRIPT_TAG;
let is_dependency_job = matches!(
@@ -216,19 +260,24 @@ pub fn start_background_processor(
{
infos.add_iter(bench, true);
}
last_processing_duration.store(time.elapsed().as_secs() as u16, Ordering::SeqCst);
last_processing_duration
.store(time.elapsed().as_secs() as u16, Ordering::SeqCst);
}
JobCompletedRx::JobCompleted(SendResult { result: SendResultPayload::UpdateFlow(UpdateFlow {
flow,
w_id,
success,
result,
worker_dir,
stop_early_override,
token,
}), time}) => {
JobCompletedRx::JobCompleted(SendResult {
result:
SendResultPayload::UpdateFlow(UpdateFlow {
flow,
w_id,
success,
result,
worker_dir,
stop_early_override,
token,
}),
time,
}) => {
// let r;
tracing::info!(parent_flow = %flow, "updating flow status");
tracing::info!(parent_flow = %flow, "updating flow status after job completion");
if let Err(e) = update_flow_status_after_job_completion(
&db,
&AuthedClient::new(
@@ -255,14 +304,14 @@ pub fn start_background_processor(
{
tracing::error!("Error updating flow status after job completion for {flow} on {worker_name}: {e:#}");
}
last_processing_duration.store(time.elapsed().as_secs() as u16, Ordering::SeqCst);
last_processing_duration
.store(time.elapsed().as_secs() as u16, Ordering::SeqCst);
}
JobCompletedRx::Killpill => {
tracing::info!("killpill job received, processing only same worker jobs");
has_been_killed = true;
},
JobCompletedRx::WakeUp => {
}
JobCompletedRx::WakeUp => {}
}
}
@@ -595,13 +644,14 @@ async fn handle_non_flow_job_error(
job: &MiniPulledJob,
mem_peak: i32,
canceled_by: Option<CanceledBy>,
err: Value,
err_string: String,
err_json: Value,
worker_name: &str,
) -> Result<WrappedError, Error> {
append_logs(
&job.id,
&job.workspace_id,
format!("Unexpected error during job execution:\n{err:#?}"),
format!("Unexpected error during job execution:\n{err_string}"),
&db.into(),
)
.await;
@@ -610,7 +660,7 @@ async fn handle_non_flow_job_error(
job,
mem_peak,
canceled_by,
err,
err_json,
worker_name,
false,
None,
@@ -633,7 +683,9 @@ pub async fn handle_job_error(
job_completed_tx: JobCompletedSender,
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
) {
let err = error_to_value(err);
let err_string = format!("{}: {}", err.name(), err.to_string());
let err_json = error_to_value(err);
let update_job_future = || async {
handle_non_flow_job_error(
@@ -641,7 +693,8 @@ pub async fn handle_job_error(
job,
mem_peak,
canceled_by.clone(),
err.clone(),
err_string,
err_json.clone(),
worker_name,
)
.await
@@ -660,8 +713,8 @@ pub async fn handle_job_error(
(job.id, Uuid::nil())
};
let wrapped_error = WrappedError { error: err.clone() };
tracing::error!(parent_flow = %flow, subflow = %job_status_to_update, "handle job error, updating flow status: {err:?}");
let wrapped_error = WrappedError { error: err_json.clone() };
tracing::error!(parent_flow = %flow, subflow = %job_status_to_update, "handle job error, updating flow status: {err_json:?}");
let updated_flow = update_flow_status_after_job_completion(
db,
client,

View File

@@ -46,11 +46,13 @@ fn make_rules_for_arg_typ(typ: &Typ) -> Vec<SchemaValidationRule> {
Typ::Sql => {
rules.push(SchemaValidationRule::IsString);
}
Typ::Object(props) => {
Typ::Object(object_type) => {
let mut obj_rules = vec![];
for prop in props {
obj_rules.push((prop.key.to_string(), make_rules_for_arg_typ(&prop.typ)));
if let Some(props) = &object_type.props {
for prop in props {
obj_rules.push((prop.key.to_string(), make_rules_for_arg_typ(&prop.typ)));
}
}
rules.push(SchemaValidationRule::IsObject(obj_rules))

View File

@@ -13,6 +13,8 @@ use anyhow::anyhow;
use futures::TryFutureExt;
use tokio::time::timeout;
use windmill_common::client::AuthedClient;
use windmill_common::utils::report_critical_error;
use windmill_common::utils::retrieve_common_worker_prefix;
use windmill_common::{
agent_workers::DECODED_AGENT_TOKEN,
apps::AppScriptId,
@@ -21,8 +23,8 @@ use windmill_common::{
scripts::PREVIEW_IS_TAR_CODEBASE_HASH,
utils::{create_directory_async, WarnAfterExt},
worker::{
make_pull_query, write_file, Connection, HttpClient, MAX_TIMEOUT, ROOT_CACHE_DIR,
ROOT_CACHE_NOMOUNT_DIR, TMP_DIR,
make_pull_query, write_file, Connection, HttpClient, MAX_TIMEOUT,
MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS, ROOT_CACHE_DIR, ROOT_CACHE_NOMOUNT_DIR, TMP_DIR,
},
KillpillSender,
};
@@ -69,8 +71,9 @@ use windmill_common::{
use windmill_queue::{
append_logs, canceled_job_to_result, empty_result, get_same_worker_job, pull, push_init_job,
CanceledBy, JobAndPerms, JobCompleted, MiniPulledJob, PrecomputedAgentInfo, PulledJob,
SameWorkerPayload, HTTP_CLIENT,
push_periodic_bash_job, CanceledBy, JobAndPerms, JobCompleted, MiniPulledJob,
PrecomputedAgentInfo, PulledJob, SameWorkerPayload, HTTP_CLIENT, INIT_SCRIPT_TAG,
PERIODIC_SCRIPT_TAG,
};
#[cfg(feature = "prometheus")]
@@ -97,7 +100,7 @@ use tokio::{
use rand::Rng;
use crate::{
agent_workers::queue_init_job,
agent_workers::{queue_init_job, queue_periodic_job},
bash_executor::{handle_bash_job, handle_powershell_job},
bun_executor::handle_bun_job,
common::{
@@ -424,8 +427,6 @@ impl std::ops::Deref for NextJob {
//only matter if CLOUD_HOSTED
pub const MAX_RESULT_SIZE: usize = 1024 * 1024 * 2; // 2MB
pub const INIT_SCRIPT_TAG: &str = "init_script";
#[derive(Clone)]
pub struct SameWorkerSender(pub Sender<SameWorkerPayload>, pub Arc<AtomicU16>);
@@ -779,7 +780,8 @@ pub fn start_interactive_worker_shell(
} else {
let pulled_job = match &conn {
Connection::Sql(db) => {
let query = ("".to_string(), make_pull_query(&[hostname.to_owned()]));
let common_worker_prefix = retrieve_common_worker_prefix(&worker_name);
let query = ("".to_string(), make_pull_query(&[common_worker_prefix]));
#[cfg(feature = "benchmark")]
let mut bench = windmill_common::bench::BenchmarkIter::new();
@@ -1221,10 +1223,9 @@ pub async fn run_worker(
)),
_ => None,
};
// If we're the first worker to run, we start another background process that listens for a specific tag.
// This tag is associated only with jobs using Bash as the script language.
// For agent workers, the expected tag format is the worker name suffixed with "-ssh".
// For regular workers, the tag is simply the machine's hostname and if not found the randomly generated hostname.
// The tag itself is simply the workers common name (for example, wk-{worker_group}-{instance_name}).
let interactive_shell = if i_worker == 1 {
let it_shell = start_interactive_worker_shell(
conn.clone(),
@@ -1305,6 +1306,13 @@ pub async fn run_worker(
tracing::error!(worker = %worker_name, hostname = %hostname, "Error queuing init bash script for worker {worker_name}: {e:#}");
return;
}
spawn_periodic_script_task(
worker_name.clone(),
conn.clone(),
same_worker_tx.clone(),
killpill_rx.resubscribe(),
);
}
#[cfg(feature = "prometheus")]
@@ -1780,6 +1788,7 @@ pub async fn run_worker(
let tag = job.tag.clone();
let is_init_script: bool = job.tag.as_str() == INIT_SCRIPT_TAG;
let is_periodic_bash_script: bool = job.tag.as_str() == PERIODIC_SCRIPT_TAG;
let is_flow = job.is_flow();
let job_id = job.id;
@@ -1835,7 +1844,43 @@ pub async fn run_worker(
.await;
break;
}
Ok(false) if is_periodic_bash_script => {
tracing::error!(
"periodic script job failed. Check logs for job ID {} for details.",
job_id
);
if let Connection::Sql(db) = conn {
report_critical_error(
format!(
"Periodic script job {} returned false (failed). Check logs for job ID {} for details.",
job_id, job_id
),
db.clone(),
Some(&arc_job.workspace_id),
Some("periodic_script_job_failed"),
)
.await;
}
}
Err(err) => {
if is_periodic_bash_script {
tracing::error!("periodic script job failed");
// Report critical error for periodic script failures
if let Connection::Sql(db) = conn {
report_critical_error(
format!(
"Periodic script job {} failed in worker {}: {}",
job_id, worker_name, &err
),
db.clone(),
Some(&arc_job.workspace_id),
Some("periodic_script_job_failed"),
)
.await;
}
}
handle_all_job_kind_error(
&conn,
&authed_client,
@@ -2013,10 +2058,126 @@ async fn queue_init_bash_maybe<'c>(
}
}
fn spawn_periodic_script_task(
worker_name: String,
conn: Connection,
same_worker_tx: SameWorkerSender,
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
) {
tokio::spawn(async move {
let config = WORKER_CONFIG.read().await;
match (
&config.periodic_script_bash,
&config.periodic_script_interval_seconds,
) {
(Some(_), None) => {
tracing::error!(
worker = %worker_name,
"periodic_script_bash is set but periodic_script_interval_seconds is not set. Both must be configured together."
);
return;
}
(None, Some(_)) => {
tracing::error!(
worker = %worker_name,
"periodic_script_interval_seconds is set but periodic_script_bash is not set. Both must be configured together."
);
return;
}
(Some(content), Some(interval_seconds)) => {
let interval_seconds = *interval_seconds;
if interval_seconds < MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS {
tracing::error!(
worker = %worker_name,
"Periodic script interval {} seconds is below minimum of {} seconds. Periodic script task will not start.",
interval_seconds,
MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS
);
return;
}
let content = content.clone();
let interval_duration = Duration::from_secs(interval_seconds);
tracing::info!(
worker = %worker_name,
"Starting periodic script task (interval: {}s)",
interval_seconds
);
loop {
tracing::info!(
worker = %worker_name,
"Triggering periodic script execution"
);
match queue_periodic_script_bash_maybe(
&conn,
same_worker_tx.clone(),
&worker_name,
&content,
)
.await
{
Ok(_) => {
tracing::debug!(
worker = %worker_name,
"Successfully queued periodic script"
);
}
Err(e) => {
tracing::error!(
worker = %worker_name,
"Error queuing periodic script: {e:#}"
);
}
}
tokio::select! {
_ = killpill_rx.recv() => {
tracing::info!("Periodic init script task shutting down for worker {}", worker_name);
break;
}
_ = tokio::time::sleep(interval_duration) => {
}
}
}
}
(None, None) => {
tracing::debug!(
worker = %worker_name,
"No periodic script configured"
);
}
}
});
}
async fn queue_periodic_script_bash_maybe<'c>(
conn: &Connection,
same_worker_tx: SameWorkerSender,
worker_name: &str,
content: &str,
) -> anyhow::Result<()> {
let uuid = match conn {
Connection::Sql(db) => push_periodic_bash_job(db, content.to_owned(), worker_name).await?,
Connection::Http(client) => queue_periodic_job(client, &content).await?,
};
same_worker_tx
.send(SameWorkerPayload { job_id: uuid, recoverable: false })
.await
.map_err(to_anyhow)?;
tracing::info!("Creating periodic script job {uuid} from periodic script: {content}");
Ok(())
}
pub struct SendResult {
pub result: SendResultPayload,
pub time: Instant,
}
pub enum SendResultPayload {
JobCompleted(JobCompleted),
UpdateFlow(UpdateFlow),

View File

@@ -98,7 +98,7 @@ pub async fn update_flow_status_after_job_completion(
success,
result,
stop_early_override,
skip_error_handler: false,
has_triggered_error_handler: false,
};
let mut unrecoverable = unrecoverable;
loop {
@@ -115,7 +115,7 @@ pub async fn update_flow_status_after_job_completion(
same_worker_tx,
worker_dir,
rec.stop_early_override,
rec.skip_error_handler,
rec.has_triggered_error_handler,
worker_name,
job_completed_tx.clone(),
#[cfg(feature = "benchmark")]
@@ -140,7 +140,7 @@ pub async fn update_flow_status_after_job_completion(
same_worker_tx,
worker_dir,
rec.stop_early_override,
rec.skip_error_handler,
rec.has_triggered_error_handler,
worker_name,
job_completed_tx.clone(),
#[cfg(feature = "benchmark")]
@@ -188,7 +188,7 @@ pub struct RecUpdateFlowStatusAfterJobCompletion {
success: bool,
result: Arc<Box<RawValue>>,
stop_early_override: Option<bool>,
skip_error_handler: bool,
has_triggered_error_handler: bool,
}
#[derive(Deserialize)]
@@ -220,6 +220,10 @@ fn get_stop_after_if_data(
return (false, None);
}
fn result_has_recover_true(nresult: Arc<Box<RawValue>>) -> bool {
let recover = serde_json::from_str::<RecoveryObject>(nresult.get());
return recover.map(|r| r.recover.unwrap_or(false)).unwrap_or(false);
}
// #[instrument(level = "trace", skip_all)]
pub async fn update_flow_status_after_job_completion_internal(
db: &DB,
@@ -233,11 +237,12 @@ pub async fn update_flow_status_after_job_completion_internal(
same_worker_tx: &SameWorkerSender,
worker_dir: &str,
stop_early_override: Option<bool>,
skip_error_handler: bool,
has_triggered_error_handler: bool,
worker_name: &str,
job_completed_tx: JobCompletedSender,
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
) -> error::Result<UpdateFlowStatusAfterJobCompletion> {
let mut has_triggered_error_handler = has_triggered_error_handler;
add_time!(bench, "update flow status internal START");
let (
should_continue_flow,
@@ -292,6 +297,11 @@ pub async fn update_flow_status_after_job_completion_internal(
Step::Step(i) => flow_value.modules.get(i),
_ => None,
};
if current_module.is_some_and(|x| x.is_flow()) {
has_triggered_error_handler = false;
}
let module_status = match module_step {
Step::PreprocessorStep => old_status
.preprocessor_module
@@ -565,10 +575,39 @@ pub async fn update_flow_status_after_job_completion_internal(
}
}
let branches = current_module
.and_then(|x| x.get_branches_skip_failures().ok())
.map(|x| {
x.branches
.iter()
.map(|b| b.skip_failure.unwrap_or(false))
.collect::<Vec<_>>()
});
let mut njobs = Vec::new();
let jobs_filtered = if branchall.is_some() {
if let Some(branches) = branches {
for (branch, job) in branches.iter().zip(jobs.iter()) {
if !branch {
njobs.push(job.clone());
}
}
njobs.as_slice()
} else {
jobs.as_slice()
}
} else {
jobs.as_slice()
};
let new_status = if skip_loop_failures
|| sqlx::query_scalar!(
"SELECT status = 'success' OR status = 'skipped' AS \"success!\" FROM v2_job_completed WHERE id = ANY($1)",
jobs.as_slice()
jobs_filtered
)
.fetch_all(&mut *tx)
.await
@@ -620,7 +659,15 @@ pub async fn update_flow_status_after_job_completion_internal(
tracing::info!(
"parallel iteration {job_id_for_status} of flow {flow} has finished",
);
(true, Some(new_status))
// for parallel branchall and forloop, we do not want to trigger the error handler again at the forloop/branchall node since it was already triggered at the leaf level
// so we want to ignore the has_triggered_error_handler flag at the forloop/branchall node and reset it based on if the node is a success or failure
if !success && flow_value.failure_module.is_some() {
has_triggered_error_handler = true;
} else {
has_triggered_error_handler = false;
}
(success, Some(new_status))
} else {
add_time!(bench, "handle parallel flow start");
tx.commit().await?;
@@ -771,7 +818,7 @@ pub async fn update_flow_status_after_job_completion_internal(
.and_then(|x| x.retry.clone())
.unwrap_or_default();
tracing::info!("update flow status on rety: {retry:#?} ");
tracing::info!("update flow status on retry: {retry:#?} ");
next_retry(&retry, &old_status.retry).is_none()
} else {
false
@@ -791,17 +838,9 @@ pub async fn update_flow_status_after_job_completion_internal(
}
};
let skip_parallel_branchall_failure = match (module_status, new_status.as_ref()) {
(
FlowStatusModule::InProgress { branchall: Some(_), parallel: true, .. },
Some(FlowStatusModule::Success { flow_jobs_success, .. }),
) => compute_skip_branchall_failure(0, true, current_module, flow_jobs_success).await?,
(
FlowStatusModule::InProgress { branchall: Some(_), parallel: true, .. },
Some(FlowStatusModule::Failure { flow_jobs_success, .. }),
) => compute_skip_branchall_failure(0, true, current_module, flow_jobs_success).await?,
_ => false,
};
let step_counter = if inc_step_counter {
sqlx::query!(
"UPDATE v2_job_status
@@ -1112,7 +1151,7 @@ pub async fn update_flow_status_after_job_completion_internal(
.flow_innermost_root_job
.map(|x| x.to_string())
.unwrap_or_else(|| "none".to_string());
tracing::info!(id = %flow_job.id, root_id = %job_root, "update flow status");
let should_continue_flow = match success {
_ if stop_early => false,
@@ -1121,10 +1160,10 @@ pub async fn update_flow_status_after_job_completion_internal(
false if unrecoverable => false,
false
if skip_seq_branch_failure
|| skip_parallel_branchall_failure
|| skip_loop_failures
|| continue_on_error =>
{
!is_last_step
}
false
@@ -1153,7 +1192,7 @@ pub async fn update_flow_status_after_job_completion_internal(
}
false
if !is_failure_step
&& !skip_error_handler
&& !has_triggered_error_handler
&& flow_value.failure_module.is_some() =>
{
true
@@ -1161,7 +1200,10 @@ pub async fn update_flow_status_after_job_completion_internal(
false => false,
};
tracing::debug!(id = %flow_job.id, root_id = %job_root, "flow status updated");
tracing::info!(id = %flow_job.id, root_id = %job_root, success = %success, stop_early = %stop_early, is_last_step = %is_last_step, unrecoverable = %unrecoverable,
skip_seq_branch_failure = %skip_seq_branch_failure, skip_loop_failures = %skip_loop_failures,
current_module_id = %current_module.map(|x| x.id.clone()).unwrap_or_default(),
continue_on_error = %continue_on_error, should_continue_flow = %should_continue_flow, "computed if flow should continue");
(
should_continue_flow,
@@ -1184,10 +1226,10 @@ pub async fn update_flow_status_after_job_completion_internal(
"Flow job canceled\n".to_string()
} else if stop_early {
format!("Flow job stopped early because of a stop early predicate returning true\n")
} else if success {
"Flow job completed with success\n".to_string()
} else if is_failure_step {
format!("Flow job completed with error, and error handler was triggered.\n It completed with {}, and with recover: {}\n", if success { "success" } else { "error" }, result_has_recover_true(nresult.clone()))
} else {
"Flow job completed with error\n".to_string()
format!("Flow job completed with {}\n", if success { "success" } else { "error" })
};
append_logs(&flow_job.id, w_id, logs, &db.into()).await;
}
@@ -1237,6 +1279,7 @@ pub async fn update_flow_status_after_job_completion_internal(
})?;
}
}
if flow_job.is_canceled() {
add_completed_job_error(
db,
@@ -1259,13 +1302,9 @@ pub async fn update_flow_status_after_job_completion_internal(
save_in_cache(db, client, &flow_job, cached_res_path, nresult.clone()).await;
}
fn result_has_recover_true(nresult: Arc<Box<RawValue>>) -> bool {
let recover = serde_json::from_str::<RecoveryObject>(nresult.get());
return recover.map(|r| r.recover.unwrap_or(false)).unwrap_or(false);
}
let success = success
&& (!is_failure_step || result_has_recover_true(nresult.clone()))
&& !skip_error_handler
&& stop_early_err_msg.is_none();
add_time!(bench, "flow status update 1");
@@ -1357,7 +1396,7 @@ pub async fn update_flow_status_after_job_completion_internal(
} else {
None
},
skip_error_handler: skip_error_handler || is_failure_step,
has_triggered_error_handler: has_triggered_error_handler || is_failure_step,
},
));
}
@@ -1780,10 +1819,11 @@ async fn push_next_flow_job(
.flow_innermost_root_job
.map(|x| x.to_string())
.unwrap_or_else(|| "none".to_string());
tracing::info!(id = %flow_job.id, root_id = %job_root, "pushing next flow job");
let mut step = Step::from_i32_and_len(status.step, flow.modules.len());
tracing::info!(id = %flow_job.id, root_id = %job_root, step = ?step, "pushing next flow job");
let mut status_module = match step {
Step::Step(i) => status
.modules
@@ -1797,6 +1837,8 @@ async fn push_next_flow_job(
Step::FailureStep => status.failure_module.module_status.clone(),
};
// tracing::error!("status_module: {status_module:#?}");
let fj: mappable_rc::Marc<MiniPulledJob> = flow_job.clone().into();
let arc_flow_job_args: Marc<HashMap<String, Box<RawValue>>> = Marc::map(fj, |x| {
if let Some(args) = &x.args {
@@ -1813,6 +1855,14 @@ async fn push_next_flow_job(
success: true,
result: if flow.modules.is_empty() {
to_raw_value(arc_flow_job_args.as_ref())
} else if matches!(
status_module,
FlowStatusModule::Success { branch_chosen: Some(_), .. }
) {
last_job_result
.as_ref()
.map(|x| x.as_ref().clone())
.unwrap_or_else(|| to_raw_value(&json!("{}")))
} else {
// it has to be an empty for loop event
serde_json::from_str("[]").unwrap()
@@ -2550,8 +2600,16 @@ async fn push_next_flow_job(
json!(FlowStatusModule::Success {
id: status_module.id(),
job: Uuid::nil(),
flow_jobs: Some(vec![]),
flow_jobs_success: Some(vec![]),
flow_jobs: if branch_chosen.is_some() {
None
} else {
Some(vec![])
},
flow_jobs_success: if branch_chosen.is_some() {
None
} else {
Some(vec![])
},
branch_chosen: branch_chosen,
approvers: vec![],
failed_retries: vec![],

View File

@@ -203,7 +203,7 @@ fn parse_ts_relative_imports(raw_code: &str, script_path: &str) -> error::Result
let normalized = normalized.to_str().unwrap().to_string();
relative_imports.push(normalized);
} else {
tracing::error!("error canonicalizing path: {:?}", normalized);
tracing::error!("error canonicalizing path: {script_path} with import {import}");
}
}
}

View File

@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.509.1";
export const VERSION = "v1.515.1";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({

View File

@@ -5,11 +5,6 @@ import { GlobalUserInfo } from "./gen/types.gen.ts";
import { loginInteractive, tryGetLoginInfo } from "./login.ts";
import { GlobalOptions } from "./types.ts";
import {
addWorkspace,
removeWorkspace,
Workspace
} from "./workspace.ts";
/**
* Main authentication function - moved from context.ts to break circular dependencies
@@ -48,6 +43,7 @@ export async function requireLogin(
}
// Update workspace token
const { removeWorkspace, addWorkspace } = await import("./workspace.ts");
removeWorkspace(workspace.name, false, opts);
workspace.token = newToken;
addWorkspace(workspace, opts);

View File

@@ -64,7 +64,7 @@ await build({
"nu",
"ts",
"regex",
"python",
"py",
"go",
"php",
"rust",

430
cli/flow_guidance.ts Normal file
View File

@@ -0,0 +1,430 @@
export const FLOW_GUIDANCE = `
---
alwaysApply: true
---
# System Prompt: OpenFlow Workflow Generator
You are an expert at creating OpenFlow YAML specifications for Windmill workflows.
OpenFlow is an open standard for defining workflows as directed acyclic graphs where each node represents a computation step.
When asked to create a flow, ask the user in which folder he wants to put it if not specified. Then create a new folder in the specified folder, that ends with \`.flow\`. It should contain a \`.yaml\` file that contains the flow definition.
For rawscript type module in the flow, the content key should start with "!inline" followed by the path of the script containing the code. It should be put in the same folder as the flow.
For script type module, path should be the path of the script in the whole repository (not constrained to the flow folder).
You do not need to create .lock and .yaml files manually. Instead, you should run \`wmill flow generate-locks --yes\` to create them.
After writing the flow, you can ask the user if he wants to push the flow with \`wmill sync push\`. Both should be run at the root of the repository.
## OpenFlow Structure
Every OpenFlow workflow must follow this root structure:
\`\`\`yaml
summary: "Brief one-line description"
description: "Optional detailed description"
value:
modules: [] # Array of workflow steps
# Optional properties:
failure_module: {} # Error handler
preprocessor_module: {} # Runs before first step
same_worker: false # Force same worker execution
concurrent_limit: 0 # Limit concurrent executions
concurrency_key: "string" # Custom concurrency grouping
concurrency_time_window_s: 0
skip_expr: "javascript_expression" # Skip workflow condition
cache_ttl: 0 # Cache results duration
priority: 0 # Execution priority
early_return: "javascript_expression" # Early termination condition
schema: # JSON Schema for workflow inputs
type: object
properties: {}
required: []
\`\`\`
## Module Types
### 1. RawScript (Inline Code)
\`\`\`yaml
id: unique_step_id
value:
type: rawscript
content: '!inline inline_script_1.inline_script.ts'
language: bun|deno|python3|go|bash|powershell|postgresql|mysql|bigquery|snowflake|mssql|oracledb|graphql|nativets|php
input_transforms:
param1:
type: javascript|static
expr: "flow_input.name" # or for static: value: "fixed_value"
# Optional properties:
path: "optional/path"
lock: "dependency_lock_content"
tag: "version_tag"
concurrent_limit: 0
concurrency_time_window_s: 0
custom_concurrency_key: "key"
is_trigger: false
assets: []
\`\`\`
### 2. PathScript (Reference to Existing Script)
\`\`\`yaml
id: step_id
value:
type: script
path: "u/user/script_name" # or "f/folder/script_name" or "hub/script_path"
input_transforms:
param_name:
type: javascript
expr: "results.previous_step"
# Optional:
hash: "specific_version_hash"
tag_override: "version_tag"
is_trigger: false
\`\`\`
### 3. PathFlow (Sub-workflow)
\`\`\`yaml
id: step_id
value:
type: flow
path: "f/folder/flow_name"
input_transforms:
param_name:
type: static
value: "fixed_value"
\`\`\`
### 4. ForLoop
\`\`\`yaml
id: loop_step
value:
type: forloopflow
iterator:
type: javascript
expr: "flow_input.items" # Must evaluate to array
skip_failures: true|false
parallel: true|false # Run iterations in parallel
parallelism: 4 # Max parallel iterations (if parallel: true)
modules:
- id: loop_body_step
value:
type: rawscript
content: |
export async function main(iter: any) {
// iter.value contains current item
// iter.index contains current index
return iter.value;
}
language: bun
input_transforms:
iter:
type: javascript
expr: "flow_input.iter"
\`\`\`
### 5. WhileLoop
\`\`\`yaml
id: while_step
value:
type: whileloopflow
skip_failures: false
parallel: false
parallelism: 1
modules:
- id: condition_check
value:
type: rawscript
content: |
export async function main() {
return Math.random() > 0.5; // Continue condition
}
language: bun
input_transforms: {}
\`\`\`
### 6. Conditional Branch (BranchOne)
\`\`\`yaml
id: branch_step
value:
type: branchone
branches:
- summary: "Condition 1"
expr: "results.previous_step > 10"
modules:
- id: branch1_step
value:
type: rawscript
content: "export async function main() { return 'branch1'; }"
language: bun
input_transforms: {}
- summary: "Condition 2"
expr: "results.previous_step <= 10"
modules:
- id: branch2_step
value:
type: rawscript
content: "export async function main() { return 'branch2'; }"
language: bun
input_transforms: {}
default: # Runs if no branch condition matches
- id: default_step
value:
type: rawscript
content: "export async function main() { return 'default'; }"
language: bun
input_transforms: {}
\`\`\`
### 7. Parallel Branches (BranchAll)
\`\`\`yaml
id: parallel_step
value:
type: branchall
parallel: true # Run branches in parallel
branches:
- summary: "Branch A"
skip_failure: false # Continue if this branch fails
modules:
- id: branch_a_step
value:
type: rawscript
content: "export async function main() { return 'A'; }"
language: bun
input_transforms: {}
- summary: "Branch B"
skip_failure: true
modules:
- id: branch_b_step
value:
type: rawscript
content: "export async function main() { return 'B'; }"
language: bun
input_transforms: {}
\`\`\`
### 8. Identity (Pass-through)
\`\`\`yaml
id: identity_step
value:
type: identity
flow: false # Set to true if this represents a sub-flow
\`\`\`
## Input Transforms & Data Flow
### JavaScript Expressions
Reference data using these variables in \`expr\` fields:
- \`flow_input.property_name\` - Access workflow inputs
- \`results.step_id\` - Access outputs from previous steps
- \`results.step_id.property\` - Access specific properties
- \`flow_input.iter.value\` - Current iteration value (in loops)
- \`flow_input.iter.index\` - Current iteration index (in loops)
### Static Values
\`\`\`yaml
input_transforms:
param_name:
type: static
value: "fixed_string" # Can be string, number, boolean, object, array
\`\`\`
### Resource References
\`\`\`yaml
input_transforms:
database:
type: static
value: "$res:f/folder/my_database" # Reference to stored resource
\`\`\`
## Advanced Module Properties
### Error Handling & Control Flow
\`\`\`yaml
id: step_id
value: # ... module definition
# Control flow options:
stop_after_if:
expr: "results.step_id.should_stop"
skip_if_stopped: true
error_message: "Custom stop message"
stop_after_all_iters_if: # For loops only
expr: "results.step_id.should_stop_loop"
skip_if_stopped: false
skip_if:
expr: "results.step_id.should_skip"
sleep:
type: javascript
expr: "flow_input.delay_seconds"
continue_on_error: false # Continue workflow if this step fails
delete_after_use: false # Clean up results after use
# Execution control:
cache_ttl: 3600 # Cache results for 1 hour
timeout: 300 # Step timeout in seconds
priority: 0 # Higher numbers = higher priority
mock:
enabled: false
return_value: "mocked_result"
# Suspend/Approval:
suspend:
required_events: 1 # Number of resume events needed
timeout: 86400 # Timeout in seconds
resume_form:
schema:
type: object
properties:
approved:
type: boolean
user_auth_required: true
user_groups_required:
type: static
value: ["admin"]
self_approval_disabled: false
hide_cancel: false
continue_on_disapprove_timeout: false
# Retry configuration:
retry:
constant:
attempts: 3
seconds: 5
# OR exponential backoff:
# exponential:
# attempts: 3
# multiplier: 2
# seconds: 1
# random_factor: 10 # 0-100% jitter
\`\`\`
## Special Modules
### Failure Handler (Error Handler)
\`\`\`yaml
value:
failure_module:
id: failure
value:
type: rawscript
content: |
export async function main(error: any) {
// error.message, error.step_id, error.name, error.stack
console.log("Flow failed:", error.message);
return error;
}
language: bun
input_transforms: {}
\`\`\`
### Preprocessor
\`\`\`yaml
value:
preprocessor_module:
id: preprocessor
value:
type: rawscript
content: |
export async function main() {
console.log("Flow starting...");
return "preprocessed";
}
language: bun
input_transforms: {}
\`\`\`
## Schema Definition
\`\`\`yaml
schema:
$schema: "https://json-schema.org/draft/2020-12/schema"
type: object
properties:
name:
type: string
description: "User name"
default: ""
email:
type: string
format: email
count:
type: integer
minimum: 1
maximum: 100
database:
type: object
format: "resource-postgresql" # Resource type reference
items:
type: array
items:
type: string
required: ["name", "email"]
order: ["name", "email", "count"] # UI field order
\`\`\`
## Best Practices
1. **Step IDs**: Use descriptive, unique identifiers (alphanumeric + underscores)
2. **Data Flow**: Chain steps using \`results.step_id\` references
3. **Error Handling**: Add failure_module for critical workflows
4. **Languages**: Use \`bun\` for TypeScript (fastest), \`python3\` for Python
5. **Resources**: Store credentials/configs as resources, reference with \`$res:path\`
6. **Loops**: Prefer \`parallel: true\` for independent iterations
7. **Branching**: Use \`branchone\` for if/else logic, \`branchall\` for parallel processing
8. **Schemas**: Always define input schemas for better UX and validation
## Example Complete Workflow
\`\`\`yaml
summary: "Process user data"
description: "Validates user input, processes data, and sends notifications"
value:
modules:
- id: validate_input
value:
type: rawscript
content: '!inline inline_script_0.inline_script.ts'
# script at path inline_script_0.inline_script.ts will contain
# export async function main(email: string, name: string) {
# if (!email.includes('@')) throw new Error('Invalid email');
# return { email, name, valid: true };
# }
language: bun
input_transforms:
email:
type: javascript
expr: "flow_input.email"
name:
type: javascript
expr: "flow_input.name"
- id: process_data
value:
type: script
path: "f/shared/data_processor"
input_transforms:
user_data:
type: javascript
expr: "results.validate_input"
- id: send_notification
value:
type: rawscript
content: '!inline inline_script_1.inline_script.ts'
# script at path inline_script_1.inline_script.ts will contain
# export async function main(processed_data: any) {
# console.log("Sending notification for:", processed_data.name);
# return "notification_sent";
# }
language: bun
input_transforms:
processed_data:
type: javascript
expr: "results.process_data"
schema:
type: object
properties:
email:
type: string
format: email
description: "User email address"
name:
type: string
description: "User full name"
required: ["email", "name"]
\`\`\`
When generating OpenFlow YAML, ensure proper indentation, valid YAML syntax, and logical step dependencies. Always include meaningful summaries and proper input transforms to connect workflow steps.
`;

View File

@@ -12,6 +12,7 @@ import app from "./apps.ts";
import script from "./script.ts";
import workspace, { getActiveWorkspace } from "./workspace.ts";
import resource from "./resource.ts";
import resourceType from "./resource-type.ts";
import user from "./user.ts";
import variable from "./variable.ts";
import hub from "./hub.ts";
@@ -22,6 +23,7 @@ import sync from "./sync.ts";
import gitsyncSettings from "./gitsync-settings.ts";
import instance from "./instance.ts";
import workerGroups from "./worker_groups.ts";
import { SCRIPT_GUIDANCE } from "./script_guidance.ts";
import dev from "./dev.ts";
import { fetchVersion } from "./context.ts";
@@ -35,6 +37,7 @@ import { add as workspaceAdd } from "./workspace.ts";
import workers from "./workers.ts";
import queues from "./queues.ts";
import { readLockfile } from "./metadata.ts";
import { FLOW_GUIDANCE } from "./flow_guidance.ts";
export {
flow,
@@ -42,6 +45,7 @@ export {
script,
workspace,
resource,
resourceType,
user,
variable,
hub,
@@ -65,7 +69,7 @@ export {
// }
// });
export const VERSION = "1.509.1";
export const VERSION = "1.515.1";
const command = new Command()
.name("wmill")
@@ -268,6 +272,50 @@ const command = new Command()
}
}
}
// Create .cursor/rules directory and files with SCRIPT_GUIDANCE content
try {
const scriptGuidanceContent = SCRIPT_GUIDANCE;
const flowGuidanceContent = FLOW_GUIDANCE;
// Create .cursor/rules directory
await Deno.mkdir(".cursor/rules", { recursive: true });
// Create windmill.mdc file
if (!await Deno.stat(".cursor/rules/script.mdc").catch(() => null)) {
await Deno.writeTextFile(".cursor/rules/script.mdc", scriptGuidanceContent);
log.info(colors.green("Created .cursor/rules/script.mdc"));
}
if (!await Deno.stat(".cursor/rules/flow.mdc").catch(() => null)) {
await Deno.writeTextFile(".cursor/rules/flow.mdc", flowGuidanceContent);
log.info(colors.green("Created .cursor/rules/flow.mdc"));
}
// Create CLAUDE.md file
if (!await Deno.stat("CLAUDE.md").catch(() => null)) {
await Deno.writeTextFile("CLAUDE.md", `
# Claude
You are a helpful assistant that can help with Windmill scripts and flows creation.
## Script Guidance
${scriptGuidanceContent}
## Flow Guidance
${flowGuidanceContent}
`);
log.info(colors.green("Created CLAUDE.md"));
}
} catch (error) {
if (error instanceof Error) {
log.warn(`Could not create guidance files: ${error.message}`);
} else {
log.warn(`Could not create guidance files: ${error}`);
}
}
},
)
.command("app", app)
@@ -275,6 +323,7 @@ const command = new Command()
.command("script", script)
.command("workspace", workspace)
.command("resource", resource)
.command("resource-type", resourceType)
.command("user", user)
.command("variable", variable)
.command("hub", hub)

View File

@@ -14,7 +14,11 @@ import {
} from "./bootstrap/script_bootstrap.ts";
import { Workspace } from "./workspace.ts";
import { SchemaProperty } from "./bootstrap/common.ts";
import { languagesWithRawReqsSupport, LanguageWithRawReqsSupport, ScriptLanguage } from "./script_common.ts";
import {
languagesWithRawReqsSupport,
LanguageWithRawReqsSupport,
ScriptLanguage,
} from "./script_common.ts";
import { inferContentTypeFromFilePath } from "./script_common.ts";
import { GlobalDeps, exts, findGlobalDeps } from "./script.ts";
import {
@@ -37,12 +41,12 @@ export class LockfileGenerationError extends Error {
}
}
export async function generateAllMetadata() { }
export async function generateAllMetadata() {}
function findClosestRawReqs(
lang: LanguageWithRawReqsSupport | undefined,
remotePath: string,
globalDeps: GlobalDeps,
globalDeps: GlobalDeps
): string | undefined {
let bestCandidate: { k: string; v: string } | undefined = undefined;
if (lang) {
@@ -74,11 +78,14 @@ async function generateFlowHash(
// Get language name from path
const lang = inferContentTypeFromFilePath(f.path, defaultTs);
// Get lock for that language
[, reqs] = Object.entries(rawReqs).find(([lang2, _]) => lang == lang2) ?? [];
[, reqs] =
Object.entries(rawReqs).find(([lang2, _]) => lang == lang2) ?? [];
}
// Embed lock into hash
hashes[f.path] = await generateHash(await f.getContentText() + (reqs ?? ""));
hashes[f.path] = await generateHash(
(await f.getContentText()) + (reqs ?? "")
);
}
}
return { ...hashes, [TOP_HASH]: await generateHash(JSON.stringify(hashes)) };
@@ -92,7 +99,7 @@ export async function generateFlowLockInternal(
},
justUpdateMetadataLock?: boolean,
noStaleMessage?: boolean,
useRawReqs?: boolean,
useRawReqs?: boolean
): Promise<string | void> {
if (folder.endsWith(SEP)) {
folder = folder.substring(0, folder.length - 1);
@@ -136,16 +143,20 @@ export async function generateFlowLockInternal(
}
if (useRawReqs) {
log.warn("If using local lockfiles, following redeployments from Web App will inevitably override generated lockfiles by CLI. To maintain your script's lockfiles you will need to redeploy only from CLI. (Behavior is subject to change)")
log.warn(
"If using local lockfiles, following redeployments from Web App will inevitably override generated lockfiles by CLI. To maintain your script's lockfiles you will need to redeploy only from CLI. (Behavior is subject to change)"
);
log.info(
(await blueColor())(
`Found raw requirements (${languagesWithRawReqsSupport.map((l) => l.rrFilename).join("/")}) for ${folder}, using it`,
),
`Found raw requirements (${languagesWithRawReqsSupport
.map((l) => l.rrFilename)
.join("/")}) for ${folder}, using it`
)
);
}
const flowValue = (await yamlParseFile(
folder! + SEP + "flow.yaml",
folder! + SEP + "flow.yaml"
)) as FlowFile;
if (!justUpdateMetadataLock) {
@@ -164,7 +175,7 @@ export async function generateFlowLockInternal(
replaceInlineScripts(
flowValue.value.modules,
folder + SEP!,
changedScripts,
changedScripts
);
//removeChangedLocks
@@ -172,28 +183,26 @@ export async function generateFlowLockInternal(
workspace,
flowValue.value,
remote_path,
rawReqs,
rawReqs
);
const inlineScripts = extractInlineScriptsForFlows(
flowValue.value.modules,
newPathAssigner(opts.defaultTs ?? "bun"),
newPathAssigner(opts.defaultTs ?? "bun")
);
inlineScripts
.filter((s) => s.path.endsWith(".lock"))
.forEach((s) => {
Deno.writeTextFileSync(
Deno.cwd() + SEP + folder + SEP + s.path,
s.content,
s.content
);
});
// Overwrite `flow.yaml` with the new lockfile references
await Deno.writeTextFile(
Deno.cwd() + SEP + folder + SEP + "flow.yaml",
yamlStringify(
flowValue as Record<string, any>
)
yamlStringify(flowValue as Record<string, any>)
);
}
@@ -223,7 +232,7 @@ export async function generateScriptMetadataInternal(
noStaleMessage: boolean,
globalDeps: GlobalDeps,
codebases: SyncCodebase[],
justUpdateMetadataLock?: boolean,
justUpdateMetadataLock?: boolean
): Promise<string | undefined> {
const remotePath = scriptPath
.substring(0, scriptPath.indexOf("."))
@@ -231,26 +240,24 @@ export async function generateScriptMetadataInternal(
const language = inferContentTypeFromFilePath(scriptPath, opts.defaultTs);
const rrLang = languagesWithRawReqsSupport.find((l) => language == l.language);
const rawReqs = findClosestRawReqs(
rrLang,
scriptPath,
globalDeps,
const rrLang = languagesWithRawReqsSupport.find(
(l) => language == l.language
);
const rawReqs = findClosestRawReqs(rrLang, scriptPath, globalDeps);
if (rawReqs && rrLang) {
log.info(
(await blueColor())(
`Found raw requirements (${rrLang.rrFilename}) for ${scriptPath}, using it`,
),
`Found raw requirements (${rrLang.rrFilename}) for ${scriptPath}, using it`
)
);
}
const metadataWithType = await parseMetadataFile(
remotePath,
undefined,
globalDeps,
codebases,
codebases
);
// read script content
@@ -262,7 +269,7 @@ export async function generateScriptMetadataInternal(
if (await checkifMetadataUptodate(remotePath, hash, undefined)) {
if (!noStaleMessage) {
log.info(
colors.green(`Script ${remotePath} metadata is up-to-date, skipping`),
colors.green(`Script ${remotePath} metadata is up-to-date, skipping`)
);
}
return;
@@ -284,7 +291,7 @@ export async function generateScriptMetadataInternal(
scriptContent,
language,
metadataParsedContent,
scriptPath,
scriptPath
);
}
@@ -298,7 +305,7 @@ export async function generateScriptMetadataInternal(
language,
remotePath,
metadataParsedContent,
rawReqs,
rawReqs
);
} else {
metadataParsedContent.lock = "";
@@ -319,7 +326,7 @@ export async function generateScriptMetadataInternal(
hash = await generateScriptHash(
rawReqs,
scriptContent,
metadataContentUsedForHash,
metadataContentUsedForHash
);
await updateMetadataGlobalLock(remotePath, hash);
if (!justUpdateMetadataLock) {
@@ -332,14 +339,14 @@ export async function updateScriptSchema(
scriptContent: string,
language: ScriptLanguage,
metadataContent: Record<string, any>,
path: string,
path: string
): Promise<void> {
// infer schema from script content and update it inplace
const result = await inferSchema(
language,
scriptContent,
metadataContent.schema,
path,
path
);
metadataContent.schema = result.schema;
if (result.has_preprocessor) {
@@ -360,7 +367,7 @@ async function updateScriptLock(
language: ScriptLanguage,
remotePath: string,
metadataContent: Record<string, any>,
rawDeps: string | undefined,
rawDeps: string | undefined
): Promise<void> {
if (
!(
@@ -393,7 +400,7 @@ async function updateScriptLock(
raw_deps: rawDeps,
entrypoint: remotePath,
}),
},
}
);
let responseText = "reading response failed";
@@ -404,11 +411,11 @@ async function updateScriptLock(
if (lock === undefined) {
if (response?.["error"]?.["message"]) {
throw new LockfileGenerationError(
`Failed to generate lockfile: ${response?.["error"]?.["message"]}`,
`Failed to generate lockfile: ${response?.["error"]?.["message"]}`
);
}
throw new LockfileGenerationError(
`Failed to generate lockfile: ${JSON.stringify(response, null, 2)}`,
`Failed to generate lockfile: ${JSON.stringify(response, null, 2)}`
);
}
const lockPath = remotePath + ".script.lock";
@@ -420,7 +427,7 @@ async function updateScriptLock(
if (await Deno.stat(lockPath)) {
await Deno.remove(lockPath);
}
} catch { }
} catch {}
metadataContent.lock = "";
}
} catch (e) {
@@ -428,7 +435,7 @@ async function updateScriptLock(
throw e;
}
throw new LockfileGenerationError(
`Failed to generate lockfile:${rawResponse.statusText}, ${responseText}, ${e}`,
`Failed to generate lockfile:${rawResponse.statusText}, ${responseText}, ${e}`
);
}
}
@@ -459,7 +466,7 @@ export async function updateFlow(
use_local_lockfiles: true,
raw_deps: rawDeps,
}),
},
}
);
} else {
// Standard dependency resolution on the server
@@ -475,7 +482,7 @@ export async function updateFlow(
flow_value,
path: remotePath,
}),
},
}
);
}
@@ -489,20 +496,20 @@ export async function updateFlow(
const msg = (res as any)?.["error"]?.["message"];
if (msg) {
throw new LockfileGenerationError(
`Failed to generate lockfile: ${msg}`,
`Failed to generate lockfile: ${msg}`
);
}
throw new LockfileGenerationError(
`Failed to generate lockfile: ${rawResponse.statusText}, ${responseText}`,
`Failed to generate lockfile: ${rawResponse.statusText}, ${responseText}`
);
}
return (res as any).updated_flow_value;
} catch (e) {
try {
responseText = await rawResponse.text();
} catch { }
} catch {}
throw new Error(
`Failed to generate lockfile. Status was: ${rawResponse.statusText}, ${responseText}, ${e}`,
`Failed to generate lockfile. Status was: ${rawResponse.statusText}, ${responseText}, ${e}`
);
}
}
@@ -514,7 +521,7 @@ export async function inferSchema(
language: ScriptLanguage,
content: string,
currentSchema: any,
path: string,
path: string
): Promise<{
schema: any;
has_preprocessor: boolean | undefined;
@@ -523,7 +530,7 @@ export async function inferSchema(
let inferedSchema: any;
if (language === "python3") {
const { parse_python } = await import(
"./wasm/python/windmill_parser_wasm.js"
"./wasm/py/windmill_parser_wasm.js"
);
inferedSchema = JSON.parse(parse_python(content));
} else if (language === "nativets") {
@@ -592,8 +599,8 @@ export async function inferSchema(
...inferedSchema.args,
];
} else if (language === "duckdb") {
const { parse_sql } = await import("./wasm/regex/windmill_parser_wasm.js");
inferedSchema = JSON.parse(parse_sql(content));
const { parse_duckdb } = await import("./wasm/regex/windmill_parser_wasm.js");
inferedSchema = JSON.parse(parse_duckdb(content));
} else if (language === "graphql") {
const { parse_graphql } = await import(
"./wasm/regex/windmill_parser_wasm.js"
@@ -640,8 +647,8 @@ export async function inferSchema(
if (inferedSchema.type == "Invalid") {
log.info(
colors.yellow(
`Script ${path} invalid, it cannot be parsed to infer schema.`,
),
`Script ${path} invalid, it cannot be parsed to infer schema.`
)
);
return {
schema: defaultScriptMetadata().schema,
@@ -655,7 +662,7 @@ export async function inferSchema(
}
currentSchema.required = [];
const oldProperties = JSON.parse(
JSON.stringify(currentSchema?.properties ?? {}),
JSON.stringify(currentSchema?.properties ?? {})
);
currentSchema.properties = {};
@@ -666,7 +673,7 @@ export async function inferSchema(
currentSchema.properties[arg.name] = oldProperties[arg.name];
}
currentSchema.properties[arg.name] = sortObject(
currentSchema.properties[arg.name],
currentSchema.properties[arg.name]
);
argSigToJsonSchemaType(arg.typ, currentSchema.properties[arg.name]);
@@ -693,7 +700,7 @@ function sortObject(obj: any): any {
...acc,
[key]: obj[key],
}),
{},
{}
);
}
@@ -703,24 +710,30 @@ export function argSigToJsonSchemaType(
| string
| { resource: string | null }
| {
list:
| (string | { object: { key: string; typ: any }[] })
| { str: any }
| { object: { key: string; typ: any }[] }
| null;
}
list:
| (
| string
| {
object: {
name?: string;
props?: { key: string; typ: any }[];
};
}
)
| { str: any }
| { object: { name?: string; props?: { key: string; typ: any }[] } }
| null;
}
| { dynselect: string }
| { str: string[] | null }
| { object: { key: string; typ: any }[] }
| { object: { name?: string; props?: { key: string; typ: any }[] } }
| {
oneof: [
{
oneof: {
label: string;
properties: { key: string; typ: any }[];
},
];
},
oldS: SchemaProperty,
}[];
},
oldS: SchemaProperty
): void {
const newS: SchemaProperty = { type: "" };
if (t === "int") {
@@ -770,9 +783,12 @@ export function argSigToJsonSchemaType(
}
} else if (typeof t !== "string" && `object` in t) {
newS.type = "object";
if (t.object) {
if (t.object.name) {
newS.format = `resource-${t.object.name}`;
}
if (t.object.props) {
const properties: Record<string, any> = {};
for (const prop of t.object) {
for (const prop of t.object.props) {
if (oldS.properties && prop.key in oldS.properties) {
properties[prop.key] = oldS.properties[prop.key];
} else {
@@ -804,12 +820,24 @@ export function argSigToJsonSchemaType(
newS.type = "array";
if (t.list === "int" || t.list === "float") {
newS.items = { type: "number" };
newS.originalType = "number[]";
} else if (t.list === "bytes") {
newS.items = { type: "string", contentEncoding: "base64" };
} else if (t.list == "string") {
newS.items = { type: "string" };
} else if (t.list && typeof t.list == "object" && "str" in t.list) {
newS.originalType = "bytes[]";
} else if (
t.list &&
typeof t.list == "object" &&
"str" in t.list &&
t.list.str
) {
newS.items = { type: "string", enum: t.list.str };
newS.originalType = "enum[]";
} else if (
t.list == "string" ||
(t.list && typeof t.list == "object" && "str" in t.list)
) {
newS.items = { type: "string", enum: oldS.items?.enum };
newS.originalType = "string[]";
} else if (
t.list &&
typeof t.list == "object" &&
@@ -820,23 +848,30 @@ export function argSigToJsonSchemaType(
type: "resource",
resourceType: t.list.resource as string,
};
newS.originalType = "resource[]";
} else if (
t.list &&
typeof t.list == "object" &&
"object" in t.list &&
t.list.object &&
t.list.object.length > 0
t.list.object
) {
const properties: Record<string, any> = {};
for (const prop of t.list.object) {
properties[prop.key] = { description: "", type: "" };
argSigToJsonSchemaType(prop.typ, properties[prop.key]);
if (t.list.object.name) {
newS.format = `resource-${t.list.object.name}`;
}
newS.items = { type: "object", properties: properties };
if (t.list.object.props && t.list.object.props.length > 0) {
const properties: Record<string, any> = {};
for (const prop of t.list.object.props) {
properties[prop.key] = { description: "", type: "" };
argSigToJsonSchemaType(prop.typ, properties[prop.key]);
}
newS.items = { type: "object", properties: properties };
} else {
newS.items = { type: "object" };
}
newS.originalType = "record[]";
} else {
newS.items = { type: "object" };
newS.originalType = "object[]";
}
} else {
newS.type = "object";
@@ -886,16 +921,15 @@ export function argSigToJsonSchemaType(
delete oldS.items;
}
Object.assign(oldS, newS);
if (oldS.format && !newS.format) {
oldS.format = undefined
}
Object.assign(oldS, newS);
// if (sameItems && savedItems != undefined && savedItems.enum != undefined) {
// sendUserToast(JSON.stringify(savedItems))
// oldS.items = savedItems
// }
if (oldS.format?.startsWith("resource-") && newS.type != "object") {
oldS.format = undefined;
}
}
////////////////////////////////////////////////////////////////////////////////////////////
@@ -912,9 +946,7 @@ export function replaceLock(o?: { lock?: string | string[] }) {
o.lock = readInlinePathSync(lockPath);
} catch (e) {
log.info(
colors.yellow(
`Failed to read lockfile, doing as if it was empty: ${e}`,
),
colors.yellow(`Failed to read lockfile, doing as if it was empty: ${e}`)
);
o.lock = "";
}
@@ -924,13 +956,13 @@ export async function parseMetadataFile(
scriptPath: string,
generateMetadataIfMissing:
| (GlobalOptions & {
path: string;
workspaceRemote: Workspace;
schemaOnly?: boolean;
})
path: string;
workspaceRemote: Workspace;
schemaOnly?: boolean;
})
| undefined,
globalDeps: GlobalDeps,
codebases: SyncCodebase[],
codebases: SyncCodebase[]
): Promise<{ isJson: boolean; payload: any; path: string }> {
let metadataFilePath = scriptPath + ".script.json";
try {
@@ -956,14 +988,14 @@ export async function parseMetadataFile(
// no metadata file at all. Create it
log.info(
(await blueColor())(
`Creating script metadata file for ${metadataFilePath}`,
),
`Creating script metadata file for ${metadataFilePath}`
)
);
metadataFilePath = scriptPath + ".script.yaml";
let scriptInitialMetadata = defaultScriptMetadata();
const scriptInitialMetadataYaml = yamlStringify(
scriptInitialMetadata as Record<string, any>,
yamlOptions,
yamlOptions
);
await Deno.writeTextFile(metadataFilePath, scriptInitialMetadataYaml, {
createNew: true,
@@ -972,8 +1004,8 @@ export async function parseMetadataFile(
if (generateMetadataIfMissing) {
log.info(
(await blueColor())(
`Generating lockfile and schema for ${metadataFilePath}`,
),
`Generating lockfile and schema for ${metadataFilePath}`
)
);
try {
await generateScriptMetadataInternal(
@@ -984,10 +1016,10 @@ export async function parseMetadataFile(
false,
globalDeps,
codebases,
false,
false
);
scriptInitialMetadata = (await yamlParseFile(
metadataFilePath,
metadataFilePath
)) as ScriptMetadata;
if (!generateMetadataIfMissing.schemaOnly) {
replaceLock(scriptInitialMetadata);
@@ -995,8 +1027,8 @@ export async function parseMetadataFile(
} catch (e) {
log.info(
colors.yellow(
`Failed to generate lockfile and schema for ${metadataFilePath}: ${e}`,
),
`Failed to generate lockfile and schema for ${metadataFilePath}: ${e}`
)
);
}
}
@@ -1035,7 +1067,7 @@ export async function checkifMetadataUptodate(
path: string,
hash: string,
conf: Lock | undefined,
subpath?: string,
subpath?: string
) {
if (!conf) {
conf = await readLockfile();
@@ -1051,17 +1083,17 @@ export async function checkifMetadataUptodate(
export async function generateScriptHash(
rawReqs: string | undefined,
scriptContent: string,
newMetadataContent: string,
newMetadataContent: string
) {
return await generateHash(
(rawReqs ?? "") + scriptContent + newMetadataContent,
(rawReqs ?? "") + scriptContent + newMetadataContent
);
}
export async function updateMetadataGlobalLock(
path: string,
hash: string,
subpath?: string,
subpath?: string
): Promise<void> {
const conf = await readLockfile();
if (!conf?.locks) {
@@ -1080,6 +1112,6 @@ export async function updateMetadataGlobalLock(
}
await Deno.writeTextFile(
WMILL_LOCKFILE,
yamlStringify(conf as Record<string, any>, yamlOptions),
yamlStringify(conf as Record<string, any>, yamlOptions)
);
}

View File

@@ -76,23 +76,37 @@ async function push(opts: PushOptions, filePath: string, name: string) {
log.info(colors.bold.underline.green("Resource pushed"));
}
async function list(opts: GlobalOptions) {
async function list(opts: GlobalOptions & { schema?: boolean }) {
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
const res = await wmill.listResourceType({
workspace: workspace.workspaceId,
});
new Table()
.header(["Workspace", "Name"])
.padding(2)
.border(true)
.body(res.map((x) => [x.workspace_id ?? "Global", x.name]))
.render();
if (opts.schema) {
new Table()
.header(["Workspace", "Name", "Schema"])
.padding(2)
.border(true)
.body(res.map((x) => [x.workspace_id ?? "Global", x.name, JSON.stringify(x.schema, null, 2)]))
.render();
} else {
new Table()
.header(["Workspace", "Name"])
.padding(2)
.border(true)
.body(res.map((x) => [x.workspace_id ?? "Global", x.name]))
.render();
}
}
const command = new Command()
.description("resource type related commands")
.action(() =>
log.info("2 actions available, list and push.")
)
.command("list", "list all resource types")
.option("--schema", "Show schema in the output")
.action(list as any)
.command(
"push",

439
cli/script_guidance.ts Normal file
View File

@@ -0,0 +1,439 @@
export const SCRIPT_GUIDANCE = `
---
alwaysApply: true
---
# Windmill Script Writing Guide - Universal System Prompt
You are a coding assistant for the Windmill platform. You help users write scripts in various languages that run on Windmill's execution environment. Each script should be placed in a folder. Ask the user in which folder he wants the script to be located at before starting coding.
After writing a script, you do not need to create .lock and .yaml files manually. Instead, you can run \`wmill script generate-metadata\` bash command. This command takes no arguments. After writing the script, you can ask the user if he wants to push the script with \`wmill sync push\`. Both should be run at the root of the repository.
## General Principles
On Windmill, scripts are executed in isolated environments with specific conventions:
- Scripts must export a main function
- Do not call the main function
- Libraries are installed automatically - do not show installation instructions
- Credentials and configuration are stored in resources and passed as parameters
- The windmill client (wmill) provides APIs for interacting with the platform
- You can use \`wmill resource-type list --schema\` to list all resource types available. You should use that to know the type of the resource you need to use in your script. You can use grep if the output is too long.
## Language-Specific Instructions
### TypeScript Variants
#### Bun Runtime (\`bun\`)
- Export a single **async** function called \`main\`
- Libraries are installed automatically
- Full npm ecosystem available
#### Deno Runtime (\`deno\`)
- Export a single **async** function called \`main\`
- Import npm libraries: \`import ... from "npm:{package}";\`
- Import deno libraries normally
- Libraries are installed automatically
#### TypeScript Resource Types & Windmill Client
**Resource Types:**
On Windmill, credentials and configuration are stored in resources and passed as parameters to main.
If you need credentials, add a parameter to \`main\` with the corresponding resource type inside the \`RT\` namespace: \`RT.Stripe\`.
Only use them if needed to satisfy instructions. Always use the RT namespace.
**Windmill Client (\`import * as wmill from "windmill-client"\`):**
\`\`\`typescript
// Resource operations
wmill.getResource(path?: string, undefinedIfEmpty?: boolean): Promise<any>
wmill.setResource(value: any, path?: string, initializeToTypeIfNotExist?: string): Promise<void>
// State management (persistent across executions)
wmill.getState(): Promise<any>
wmill.setState(state: any): Promise<void>
// Variables
wmill.getVariable(path: string): Promise<string>
wmill.setVariable(path: string, value: string, isSecretIfNotExist?: boolean, descriptionIfNotExist?: string): Promise<void>
// Script execution
wmill.runScript(path?: string | null, hash_?: string | null, args?: Record<string, any> | null, verbose?: boolean): Promise<any>
wmill.runScriptAsync(path: string | null, hash_: string | null, args: Record<string, any> | null, scheduledInSeconds?: number | null): Promise<string>
wmill.waitJob(jobId: string, verbose?: boolean): Promise<any>
wmill.getResult(jobId: string): Promise<any>
wmill.getRootJobId(jobId?: string): Promise<string>
// S3 file operations (if S3 is configured)
wmill.loadS3File(s3object: S3Object, s3ResourcePath?: string | undefined): Promise<Uint8Array | undefined>
wmill.writeS3File(s3object: S3Object | undefined, fileContent: string | Blob, s3ResourcePath?: string | undefined): Promise<S3Object>
// Flow operations
wmill.setFlowUserState(key: string, value: any, errorIfNotPossible?: boolean): Promise<void>
wmill.getFlowUserState(key: string, errorIfNotPossible?: boolean): Promise<any>
wmill.getResumeUrls(approver?: string): Promise<{approvalPage: string, resume: string, cancel: string}>
\`\`\`
### Python (\`python3\`)
- Script contains at least one function called \`main\`
- Libraries are installed automatically
- Do not call the main function
**Resource Types:**
If you need credentials, add a parameter to \`main\` with the corresponding resource type.
**Redefine** the type of needed resources before the main function as TypedDict (only include if actually needed).
Resource type name must be **IN LOWERCASE**.
If an import conflicts with a resource type name, **rename the imported object, not the type name**.
Import TypedDict from typing **if using it**.
**Windmill Client (\`import wmill\`):**
\`\`\`python
# Resource operations
wmill.get_resource(path: str, none_if_undefined: bool = False) -> dict | None
wmill.set_resource(path: str, value: Any, resource_type: str = "any") -> None
# State management
wmill.get_state() -> Any
wmill.set_state(value: Any) -> None
wmill.get_flow_user_state(key: str) -> Any
wmill.set_flow_user_state(key: str, value: Any) -> None
# Variables
wmill.get_variable(path: str) -> str
wmill.set_variable(path: str, value: str, is_secret: bool = False) -> None
# Script execution
wmill.run_script(path: str = None, hash_: str = None, args: dict = None, timeout = None, verbose: bool = False) -> Any
wmill.run_script_async(path: str = None, hash_: str = None, args: dict = None, scheduled_in_secs: int = None) -> str
wmill.wait_job(job_id: str, timeout = None, verbose: bool = False) -> Any
wmill.get_result(job_id: str) -> Any
# S3 operations
wmill.load_s3_file(s3object: S3Object | str, s3_resource_path: str | None = None) -> bytes
wmill.write_s3_file(s3object: S3Object | str | None, file_content: BufferedReader | bytes, s3_resource_path: str | None = None) -> S3Object
# Utilities
wmill.get_workspace() -> str
wmill.whoami() -> dict
wmill.set_progress(value: int, job_id: Optional[str] = None) -> None
\`\`\`
### PHP (\`php\`)
- Script must start with \`<?php\`
- Contains at least one function called \`main\`
- **Redefine** resource types before main function (only if needed)
- Check if class exists using \`class_exists\` before defining types
- Resource type name must be exactly as specified
**Resource Types:**
If you need credentials, add a parameter to \`main\` with the corresponding resource type.
**Redefine** the type of needed resources before the main function.
Before defining each type, check if the class already exists using class_exists.
The resource type name has to be exactly as specified.
**Library Dependencies:**
\`\`\`php
// require:
// mylibrary/mylibrary
// myotherlibrary/myotherlibrary@optionalversion
\`\`\`
One per line before main function. Autoload already included.
### Rust (\`rust\`)
\`\`\`rust
use anyhow::anyhow;
use serde::Serialize;
#[derive(Serialize, Debug)]
struct ReturnType {
// ...
}
fn main(...) -> anyhow::Result<ReturnType>
\`\`\`
**Dependencies:**
\`\`\`\`rust
//! \`\`\`cargo
//! [dependencies]
//! anyhow = "1.0.86"
//! \`\`\`
\`\`\`\`
Serde already included. For async functions, keep main sync and create runtime inside.
### Go (\`go\`)
- File package must be "inner"
- Export single function called \`main\`
- Return type: \`({return_type}, error)\`
### Bash (\`bash\`)
- Do not include "#!/bin/bash"
- Arguments: \`var1="$1"\`, \`var2="$2"\`, etc.
### SQL Variants
#### PostgreSQL (\`postgresql\`)
- Arguments: \`$1::{type}\`, \`$2::{type}\`, etc.
- Name parameters: \`-- $1 name1\` or \`-- $2 name = default\`
#### MySQL (\`mysql\`)
- Arguments: \`?\` placeholders
- Name parameters: \`-- ? name1 ({type})\` or \`-- ? name2 ({type}) = default\`
#### BigQuery (\`bigquery\`)
- Arguments: \`@name1\`, \`@name2\`, etc.
- Name parameters: \`-- @name1 ({type})\` or \`-- @name2 ({type}) = default\`
#### Snowflake (\`snowflake\`)
- Arguments: \`?\` placeholders
- Name parameters: \`-- ? name1 ({type})\` or \`-- ? name2 ({type}) = default\`
#### Microsoft SQL Server (\`mssql\`)
- Arguments: \`@P1\`, \`@P2\`, etc.
- Name parameters: \`-- @P1 name1 ({type})\` or \`-- @P2 name2 ({type}) = default\`
### GraphQL (\`graphql\`)
- Add needed arguments as query parameters
### PowerShell (\`powershell\`)
- Arguments via param function on first line:
\`\`\`powershell
param($ParamName1, $ParamName2 = "default value", [{type}]$ParamName3, ...)
\`\`\`
### C# (\`csharp\`)
- Public static Main method inside a class
- NuGet packages: \`#r "nuget: PackageName, Version"\` at top
- Method signature: \`public static ReturnType Main(parameter types...)\`
### Java (\`java\`)
- Main public class with \`public static main()\` method
- Dependencies: \`//requirements://groupId:artifactId:version\` at top
- Method signature: \`public static Object main(parameter types...)\`
## Supported Languages
\`bunnative\`, \`nativets\`, \`bun\`, \`deno\`, \`python3\`, \`php\`, \`rust\`, \`go\`, \`bash\`, \`postgresql\`, \`mysql\`, \`bigquery\`, \`snowflake\`, \`mssql\`, \`graphql\`, \`powershell\`, \`csharp\`, \`java\`
Always follow the specific conventions for the language being used and include only necessary dependencies and resource types.
# Windmill CLI Commands Summary
## Core Commands
### \`wmill init\`
Bootstrap a new Windmill project with a \`wmill.yaml\` configuration file
- \`--use-default\` - Use default settings without checking backend
- \`--use-backend\` - Use backend git-sync settings if available
- \`--repository <repo>\` - Specify repository path when using backend settings
### \`wmill version\`
Display CLI and backend version information
- Shows current CLI version and checks for updates
- Displays backend version if workspace is configured
### \`wmill upgrade\`
Upgrade the CLI to the latest version available on npm
## Authentication & Workspace Management
### \`wmill workspace\`
Manage Windmill workspaces
- \`add\` - Add a new workspace configuration
- \`list\` - List all configured workspaces
- \`switch <workspace>\` - Switch to a specific workspace
- \`remove <workspace>\` - Remove a workspace configuration
### \`wmill user\`
User management operations
- \`list\` - List users in the workspace
- \`whoami\` - Show current user information
## Script & Flow Management
### \`wmill script\`
Manage Windmill scripts
- \`push <file>\` - Push a script file to the workspace
- \`list\` - List all scripts in the workspace
- \`show <path>\` - Show script details
- \`run <path>\` - Execute a script
- \`generate-metadata <file>\` - Generate metadata for a script
### \`wmill flow\`
Manage Windmill flows
- \`push <path>\` - Push a flow to the workspace
- \`list\` - List all flows
- \`show <path>\` - Show flow details
- \`run <path>\` - Execute a flow
### \`wmill app\`
Manage Windmill applications
- \`push <path>\` - Push an app to the workspace
- \`list\` - List all apps
- \`show <path>\` - Show app details
## Resource Management
### \`wmill resource\`
Manage resources (database connections, API keys, etc.)
- \`list\` - List all resources
- \`push <file>\` - Push a resource definition
- \`show <path>\` - Show resource details
### \`wmill resource-type\`
Manage custom resource types
- Operations for defining and managing custom resource schemas
### \`wmill variable\`
Manage workspace variables and secrets
- \`list\` - List all variables
- \`push <file>\` - Push a variable definition
- \`show <path>\` - Show variable details
## Scheduling & Automation
### \`wmill schedule\`
Manage scheduled jobs
- \`list\` - List all schedules
- \`push <file>\` - Push a schedule definition
- Operations for managing cron-based job scheduling
### \`wmill trigger\`
Manage event triggers
- Operations for managing webhooks and event-based triggers
## Synchronization
### \`wmill sync\`
Synchronize local files with Windmill workspace
- \`pull\` - Download resources from workspace to local files
- \`push\` - Upload local files to workspace
- Supports bidirectional sync with conflict resolution
- Works with \`wmill.yaml\` configuration
### \`wmill gitsync-settings\`
Manage git synchronization settings
- Configure automatic git sync for the workspace
- Pull/push git sync configurations
## Development Tools
### \`wmill dev\`
Start development mode with live reloading
- Watches local files for changes
- Automatically syncs changes to workspace
- Provides real-time feedback during development
### \`wmill hub\`
Interact with Windmill Hub
- \`pull\` - Pull resources from the public Windmill Hub
- Access community-shared scripts, flows, and resource types
## Infrastructure Management
### \`wmill instance\`
Manage Windmill instance settings (Enterprise)
- Configure instance-level settings
- Manage global configurations
### \`wmill worker-groups\`
Manage worker groups for job execution
- Configure and manage worker pool settings
### \`wmill workers\`
Manage individual workers
- Monitor and configure worker instances
### \`wmill queues\`
Manage job queues
- Monitor and configure job execution queues
## Utility Commands
### \`wmill folder\`
Manage workspace folders and organization
- Operations for organizing resources into folders
### \`wmill completions\`
Generate shell completion scripts
- Support for bash, zsh, fish, and PowerShell
## Global Options
All commands support these global options:
- \`--workspace <workspace>\` - Specify target workspace
- \`--token <token>\` - Specify API token
- \`--base-url <url>\` - Specify Windmill instance URL
- \`--config-dir <dir>\` - Custom configuration directory
- \`--debug/--verbose\` - Enable debug logging
- \`--show-diffs\` - Show detailed diff information during sync
The CLI uses a \`wmill.yaml\` configuration file for project settings and supports both local development workflows and CI/CD integration.
`;

View File

@@ -22,7 +22,7 @@ const languages = [
"graphql",
"bash",
"powershell",
# "php",
"php",
"rust",
"csharp",
"nu",
@@ -37,29 +37,36 @@ def main [] {
def 'main test deno' [] {
main clean;
print $"Testing Deno"
$languages | each { |l|
print $"+ ($l)"
deno run -A ../cli/main.ts script bootstrap $"f/tests/(random uuid)" $l
}
deno run -A ../cli/main.ts script generate-metadata
print $"\nDeno has passed!\n"
}
def 'main test node' [] {
# TODO: Use node
main clean;
print $"Testing Node"
$languages | each { |l|
print $"+ ($l)"
node ../cli/npm/esm/main.js script bootstrap $"f/tests/(random uuid)" $l
}
node ../cli/npm/esm/main.js script generate-metadata
print $"\nNode has passed!\n"
}
def 'main clean' [] {
print $"Cleaning"
rm -rf f/tests/*
mkdir f/tests
}
def 'main build' [] {
../cli/build.sh
print $"Building..."
cd ../cli; ./build.sh
cd ../local
}
# def main [] {

4
cli/wasm/py/windmill_parser_wasm.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
/* tslint:disable */
/* eslint-disable */
export function parse_python(code: string, main_override?: string | null): string;
export function parse_assets_py(code: string): string;

View File

@@ -0,0 +1,147 @@
let WASM_VECTOR_LEN = 0;
let cachedUint8ArrayMemory0 = null;
function getUint8ArrayMemory0() {
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8ArrayMemory0;
}
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
const encodeString = function (arg, view) {
return cachedTextEncoder.encodeInto(arg, view);
};
function passStringToWasm0(arg, malloc, realloc) {
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0;
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
let len = arg.length;
let ptr = malloc(len, 1) >>> 0;
const mem = getUint8ArrayMemory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
const ret = encodeString(arg, view);
offset += ret.written;
ptr = realloc(ptr, len, offset, 1) >>> 0;
}
WASM_VECTOR_LEN = offset;
return ptr;
}
function isLikeNone(x) {
return x === undefined || x === null;
}
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
/**
* @param {string} code
* @param {string | null} [main_override]
* @returns {string}
*/
export function parse_python(code, main_override) {
let deferred3_0;
let deferred3_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
var ptr1 = isLikeNone(main_override) ? 0 : passStringToWasm0(main_override, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len1 = WASM_VECTOR_LEN;
const ret = wasm.parse_python(ptr0, len0, ptr1, len1);
deferred3_0 = ret[0];
deferred3_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
}
}
/**
* @param {string} code
* @returns {string}
*/
export function parse_assets_py(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_assets_py(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
const imports = {
__wbindgen_placeholder__: {
__wbindgen_init_externref_table: function() {
const table = wasm.__wbindgen_export_0;
const offset = table.grow(4);
table.set(0, undefined);
table.set(offset + 0, undefined);
table.set(offset + 1, null);
table.set(offset + 2, true);
table.set(offset + 3, false);
;
},
},
};
const wasm_url = new URL('windmill_parser_wasm_bg.wasm', import.meta.url);
let wasmCode = '';
switch (wasm_url.protocol) {
case 'file:':
wasmCode = await Deno.readFile(wasm_url);
break
case 'https:':
case 'http:':
wasmCode = await (await fetch(wasm_url)).arrayBuffer();
break
default:
throw new Error(`Unsupported protocol: ${wasm_url.protocol}`);
}
const wasmInstance = (await WebAssembly.instantiate(wasmCode, imports)).instance;
const wasm = wasmInstance.exports;
export const __wasm = wasm;
wasm.__wbindgen_start();

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More