Compare commits

..

1 Commits

Author SHA1 Message Date
HugoCasa
8468b2cb42 fix sqlx 2026-01-29 18:50:06 +01:00
126 changed files with 374 additions and 2859 deletions

View File

@@ -96,6 +96,7 @@
"enabledPlugins": {
"rust-analyzer-lsp@claude-plugins-official": true,
"typescript-lsp@claude-plugins-official": true,
"code-review@claude-plugins-official": true
"code-review@claude-plugins-official": true,
"commit-commands@claude-plugins-official": true
}
}

View File

@@ -1,60 +0,0 @@
---
name: commit
user_invocable: true
description: Create a git commit with conventional commit format. MUST use anytime you want to commit changes.
---
# Git Commit Skill
Create a focused, single-line commit following conventional commit conventions.
## Instructions
1. **Analyze changes**: Run `git status` and `git diff` to understand what was modified
2. **Stage only modified files**: Add files individually by name. NEVER use `git add -A` or `git add .`
3. **Write commit message**: Follow the conventional commit format as a single line
## Conventional Commit Format
```
<type>: <description>
```
### Types
- `feat`: New feature or capability
- `fix`: Bug fix
- `refactor`: Code change that neither fixes a bug nor adds a feature
- `docs`: Documentation only changes
- `style`: Formatting, missing semicolons, etc (no code change)
- `test`: Adding or correcting tests
- `chore`: Maintenance tasks, dependency updates, etc
- `perf`: Performance improvement
### Rules
- Message MUST be a single line (no multi-line messages)
- Description should be lowercase, imperative mood ("add" not "added")
- No period at the end
- Keep under 72 characters total
### Examples
```
feat: add token usage tracking for AI providers
fix: resolve null pointer in job executor
refactor: extract common validation logic
docs: update API endpoint documentation
chore: upgrade sqlx to 0.7
```
## Execution Steps
1. Run `git status` to see all changes
2. Run `git diff` to understand the changes in detail
3. Run `git log --oneline -5` to see recent commit style
4. Stage ONLY the modified/relevant files: `git add <file1> <file2> ...`
5. Create the commit with conventional format:
```bash
git commit -m "<type>: <description>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"
```
6. Run `git status` to verify the commit succeeded

View File

@@ -1,87 +0,0 @@
---
name: pr
user_invocable: true
description: Open a draft pull request on GitHub. MUST use when you want to create/open a PR.
---
# Pull Request Skill
Create a draft pull request with a clear title and explicit description of changes.
## Instructions
1. **Analyze branch changes**: Understand all commits since diverging from main
2. **Push to remote**: Ensure all commits are pushed
3. **Create draft PR**: Always open as draft for review before merging
## PR Title Format
Follow conventional commit format for the PR title:
```
<type>: <description>
```
### Types
- `feat`: New feature or capability
- `fix`: Bug fix
- `refactor`: Code restructuring
- `docs`: Documentation changes
- `chore`: Maintenance tasks
- `perf`: Performance improvements
### Title Rules
- Keep under 70 characters
- Use lowercase, imperative mood
- No period at the end
## PR Body Format
The body MUST be explicit about what changed. Structure:
```markdown
## Summary
<Clear description of what this PR does and why>
## Changes
- <Specific change 1>
- <Specific change 2>
- <Specific change 3>
## Test plan
- [ ] <How to verify change 1>
- [ ] <How to verify change 2>
---
Generated with [Claude Code](https://claude.com/claude-code)
```
## Execution Steps
1. Run `git status` to check for uncommitted changes
2. Run `git log main..HEAD --oneline` to see all commits in this branch
3. Run `git diff main...HEAD` to see the full diff against main
4. Check if remote branch exists and is up to date:
```bash
git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || echo "no upstream"
```
5. Push to remote if needed: `git push -u origin HEAD`
6. Create draft PR using gh CLI:
```bash
gh pr create --draft --title "<type>: <description>" --body "$(cat <<'EOF'
## Summary
<description>
## Changes
- <change 1>
- <change 2>
## Test plan
- [ ] <test 1>
- [ ] <test 2>
---
Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
```
7. Return the PR URL to the user

View File

@@ -1,6 +1,6 @@
---
name: rust-backend
description: Rust coding guidelines for the Windmill backend. MUST use when writing or modifying Rust code in the backend directory.
description: Rust coding guidelines for the Windmill backend. Apply when writing or modifying Rust code in the backend directory.
---
# Rust Backend Coding Guidelines

View File

@@ -1,35 +1,5 @@
# Changelog
## [1.623.0](https://github.com/windmill-labs/windmill/compare/v1.622.0...v1.623.0) (2026-01-31)
### Features
* add PocketID OAuth provider support ([#7318](https://github.com/windmill-labs/windmill/issues/7318)) ([720e3c5](https://github.com/windmill-labs/windmill/commit/720e3c543623c2612b1af704c13d032c53368efb))
### Bug Fixes
* add schema compatibility layer for MCP clients like n8n ([#7747](https://github.com/windmill-labs/windmill/issues/7747)) ([297aa23](https://github.com/windmill-labs/windmill/commit/297aa23ed46315dfd4b034d44361a5bd8aaca884))
* preserve script envs field during sync push ([f405dff](https://github.com/windmill-labs/windmill/commit/f405dff2e22681dc8d4f3a9b7427e278c6cfb0cc))
## [1.622.0](https://github.com/windmill-labs/windmill/compare/v1.621.2...v1.622.0) (2026-01-29)
### Features
* add token usage tracking to AI agent output ([#7738](https://github.com/windmill-labs/windmill/issues/7738)) ([ce23f21](https://github.com/windmill-labs/windmill/commit/ce23f21c0e0bc6365f616ace4c45fa341741c555))
* workspace dedicated workers ([#7741](https://github.com/windmill-labs/windmill/issues/7741)) ([60858d1](https://github.com/windmill-labs/windmill/commit/60858d1e20e68b83fddcdbfc0ff34decaff5d1c5))
### Bug Fixes
* forward teams error to client ([#7746](https://github.com/windmill-labs/windmill/issues/7746)) ([ca8dbc0](https://github.com/windmill-labs/windmill/commit/ca8dbc0676dda619aff6fab7f6ff05ed773738e0))
* indexer build error ([#7744](https://github.com/windmill-labs/windmill/issues/7744)) ([6679ecb](https://github.com/windmill-labs/windmill/commit/6679ecb9a2ead08d2252a64f2a27a6d539fa23e9))
* remove uuid-ossp extension requirement for RDS compatibility ([ad5293c](https://github.com/windmill-labs/windmill/commit/ad5293c0edacfaf1431a3639ef5ea32d9bd761b0))
* require AGENT_TOKEN and BASE_INTERNAL_URL for agent mode ([6c84a89](https://github.com/windmill-labs/windmill/commit/6c84a8905382e29a4bbe0ae947eda794bc4dc566))
* visibility bug on deployment UI (issue when renaming items) + add tracking of folders and resource types ([#7739](https://github.com/windmill-labs/windmill/issues/7739)) ([998f11a](https://github.com/windmill-labs/windmill/commit/998f11a10da45c6d933d8b78ca24ed4f55a53f3b))
## [1.621.2](https://github.com/windmill-labs/windmill/compare/v1.621.1...v1.621.2) (2026-01-29)

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_diff\n (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)\n VALUES\n ('test-workspace', 'wm-fork-test-workspace', 'f/shared/old_name', 'resource', 0, 1, NULL),\n ('test-workspace', 'wm-fork-test-workspace', 'f/shared/new_name', 'resource', 1, 0, NULL)",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "14110b9c1b68cf29a6cbdfa737707a44bda831246736e60d63696c3227491adb"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE flow SET value = $1\n WHERE workspace_id = 'test-workspace' AND path = 'f/shared/original_flow'",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb"
]
},
"nullable": []
},
"hash": "18272dde939afcd87464d74c03bc3c8ee4395919fbfa50565d8d800e3886911e"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT token, expires_at, blacklisted_at, blacklisted_by\n FROM agent_token_blacklist\n ORDER BY blacklisted_at DESC",
"query": "SELECT token, expires_at, blacklisted_at, blacklisted_by \n FROM agent_token_blacklist \n ORDER BY blacklisted_at DESC",
"describe": {
"columns": [
{
@@ -34,5 +34,5 @@
false
]
},
"hash": "7cbfad812eeb80cff00336697052f266693cf838d62a8b1e581c7239ec42095b"
"hash": "1c5d3556fc8436ddd294f39c5431e1f501a821d6143c5d8aece20814237a6b86"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_diff\n (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)\n VALUES\n ('test-workspace', 'wm-fork-test-workspace', 'f/shared/new_in_parent', 'script', 1, 0, NULL),\n ('test-workspace', 'wm-fork-test-workspace', 'new_type', 'resource_type', 1, 0, NULL)",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "1ed8d27979cd903cfd12f52461aada26f807236364fb56bf826228983bdab3ab"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE script SET archived = true\n WHERE workspace_id = 'wm-fork-test-workspace' AND path = 'f/shared/to_delete'",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "21941635afc295e5a42aceffce7aad910477c4537e9fe40b227e4ecc7eae76d3"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_diff\n (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)\n VALUES ('test-workspace', 'wm-fork-test-workspace', 'f/shared/new_in_fork', 'script', 0, 1, NULL)",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "2248e2c5ca8e6fe58704e45066477a1257b15cde7ea44962ce8566d6d7396add"
}

View File

@@ -1,32 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT has_changes, exists_in_source, exists_in_fork FROM workspace_diff\n WHERE path = 'f/shared/new_in_parent' AND kind = 'script' AND source_workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "has_changes",
"type_info": "Bool"
},
{
"ordinal": 1,
"name": "exists_in_source",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "exists_in_fork",
"type_info": "Bool"
}
],
"parameters": {
"Left": []
},
"nullable": [
true,
true,
true
]
},
"hash": "338f8f878aba9d9361b17329ee448ee4582194cbbc853d26abd285c259642c60"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE script\n SET content = 'def main(): return \"fork_modified\"', summary = 'Modified in fork'\n WHERE workspace_id = 'wm-fork-test-workspace' AND path = 'f/shared/to_modify_fork'",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "33ad6eb46a13dd49605fdb863a03602878c9d1f40f61e349e06698140133153f"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO script (workspace_id, path, hash, content, summary, description, language, created_by, created_at, archived, schema_validation, ws_error_handler_muted, deleted)\n VALUES\n ('test-workspace', 'f/shared/original_script', 12345, 'def main(): pass', 'Original', '', 'python3', 'test@windmill.dev', NOW(), false, false, false, false),\n ('test-workspace', 'f/shared/to_modify_parent', 22222, 'def main(): return 1', 'To modify in parent', '', 'python3', 'test@windmill.dev', NOW(), false, false, false, false),\n ('test-workspace', 'f/shared/to_modify_fork', 33333, 'def main(): return 2', 'To modify in fork', '', 'python3', 'test@windmill.dev', NOW(), false, false, false, false),\n ('test-workspace', 'f/shared/to_conflict', 44444, 'def main(): return 3', 'To conflict', '', 'python3', 'test@windmill.dev', NOW(), false, false, false, false),\n ('test-workspace', 'f/shared/to_delete', 55555, 'def main(): return 4', 'To delete', '', 'python3', 'test@windmill.dev', NOW(), false, false, false, false)",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "3439a44ea40ea748346af50bc1529dc8fd135c8cb54b7e39d03b326c30a0ab25"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n tag\n FROM\n v2_job\n WHERE\n id = $1\n ",
"query": "\n SELECT \n tag\n FROM \n v2_job\n WHERE \n id = $1\n ",
"describe": {
"columns": [
{
@@ -18,5 +18,5 @@
false
]
},
"hash": "1d32bd9309bf2066399b446e8c47502a0ec72ffc07b22593311915ab5e98f80a"
"hash": "49e2430af74ec10857e5df7f7e1ad1b53ba70bb51b0259a1f765f76db9b733ad"
}

View File

@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id FROM app WHERE path = 'f/shared/dashboard' AND workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "49e4bef9d5e366c179d33794472201bfca768ee5ead967e0232fa87a59b9747c"
}

View File

@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO agent_token_blacklist (token, expires_at, blacklisted_by)\n VALUES ($1, $2, $3)\n ON CONFLICT (token) DO UPDATE SET\n expires_at = EXCLUDED.expires_at,\n blacklisted_at = NOW(),\n blacklisted_by = EXCLUDED.blacklisted_by",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Timestamp",
"Varchar"
]
},
"nullable": []
},
"hash": "4c1dac4ddeca8f053abb35c833acd6993448d0327404cf1ad089ec3df6f2db35"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE script\n SET content = 'def main(): return \"modified\"', summary = 'Modified in parent'\n WHERE workspace_id = 'test-workspace' AND path = 'f/shared/to_modify_parent'",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "4ef7fcfa9a8962497c1eace31e59138cb8b252b207122a823185b36c44c10045"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_diff\n (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)\n VALUES\n ('test-workspace', 'wm-fork-test-workspace', 'f/shared/to_modify_fork', 'script', 0, 1, NULL),\n ('test-workspace', 'wm-fork-test-workspace', 'f/shared/resource_to_modify', 'resource', 0, 1, NULL),\n ('test-workspace', 'wm-fork-test-workspace', 'shared', 'folder', 0, 1, NULL)",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "56e6be86747e953e4eb75b434906bea7f471c22923ac29894feae9c199d78788"
}

View File

@@ -1,17 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_diff (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)\n VALUES ($1, $2, $3, $4, 1, 0, NULL)\n ON CONFLICT (source_workspace_id, fork_workspace_id, path, kind)\n DO UPDATE SET\n ahead = workspace_diff.ahead + 1,\n has_changes = NULL",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "5a3cf6f0958a559c147a37e29458b48eb7488446eb69adeddf2007ae7ae897a7"
}

View File

@@ -1,17 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_diff (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)\n SELECT $1, unnest($2::varchar[]), $3, $4, 0, 1, NULL\n ON CONFLICT (source_workspace_id, fork_workspace_id, path, kind)\n DO UPDATE SET\n behind = workspace_diff.behind + 1,\n has_changes = NULL",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"VarcharArray",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "5b3b4119469b7b110657926e6f99093c8fa5980ad061804cf73dc29151beac4b"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO resource_type (workspace_id, name, schema, description, created_by)\n VALUES ('test-workspace', 'custom_db', $1, 'Custom DB type', 'test@windmill.dev')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb"
]
},
"nullable": []
},
"hash": "6974521dec19adbc4c4497cc50e11a3f3be2a76a68287b3da2b999a925f972b4"
}

View File

@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT has_changes FROM workspace_diff\n WHERE path = 'f/shared/original_script' AND kind = 'script' AND source_workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "has_changes",
"type_info": "Bool"
}
],
"parameters": {
"Left": []
},
"nullable": [
true
]
},
"hash": "77929b1ab186a01113436b5739240285dc111218efb4df8dbb0c72280a4c6a2c"
}

View File

@@ -1,35 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT schema, description, format_extension\n FROM resource_type\n WHERE workspace_id = $1 AND name = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "schema",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "format_extension",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
true,
true,
true
]
},
"hash": "7bc9fc05dbd162866bef1fdd3e7faeb50429881ed1bc962903f06e4b3d5f8d44"
}

View File

@@ -1,41 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT display_name, owners, extra_perms, summary\n FROM folder\n WHERE workspace_id = $1 AND name = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "display_name",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "owners",
"type_info": "VarcharArray"
},
{
"ordinal": 2,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 3,
"name": "summary",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
true
]
},
"hash": "8039b459914ceefe0c0a31b97473a5522e47b4b2fd3ddeff8f221560bc9c6f57"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_diff\n (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)\n VALUES ('test-workspace', 'wm-fork-test-workspace', 'f/shared/lazy_test', 'script', 1, 0, NULL)\n ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "85711709ee4c9684cd46af85c2d21124bfaa6aa07eadeb8d55b4b9b9e3073f10"
}

View File

@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT has_changes FROM workspace_diff\n WHERE path = 'f/shared/lazy_test' AND kind = 'script' AND source_workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "has_changes",
"type_info": "Bool"
}
],
"parameters": {
"Left": []
},
"nullable": [
true
]
},
"hash": "8c0596e2a1c7cf9eef4e4ce3249a8bdd6cb4ead841591d873e5ec8c9c214b6b5"
}

View File

@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM workspace WHERE id = 'wm-fork-test-workspace')",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "8d1920926cbdce897305c775b1d286fed8fa2f4258bde54b9d771c91923751ff"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO deployment_metadata (workspace_id, path, script_hash, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (workspace_id, script_hash) WHERE script_hash IS NOT NULL DO UPDATE SET callback_job_ids = EXCLUDED.callback_job_ids, deployment_msg = EXCLUDED.deployment_msg",
"query": "INSERT INTO deployment_metadata (workspace_id, path, script_hash, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5) \n ON CONFLICT (workspace_id, script_hash) WHERE script_hash IS NOT NULL DO UPDATE SET callback_job_ids = EXCLUDED.callback_job_ids, deployment_msg = EXCLUDED.deployment_msg",
"describe": {
"columns": [],
"parameters": {
@@ -14,5 +14,5 @@
},
"nullable": []
},
"hash": "c64288c867ba944e834a44c5a6af7231efd899a148d3607316a5537f4e7b031c"
"hash": "8e750d4b3af9b5844c11b1b92741f2ee9d2d3412d4a2c96c6ccc87ec1c382384"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO resource_type (workspace_id, name, schema, description, created_by)\n VALUES ('test-workspace', 'new_type', $1, 'New type in parent', 'test@windmill.dev')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb"
]
},
"nullable": []
},
"hash": "907025d53448ea70760420f90cd95ad1d93ef6d4ebe0295cdc1e70108320f6a9"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_diff\n (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)\n VALUES ('test-workspace', 'wm-fork-test-workspace', 'f/shared/original_script', 'script', 0, 0, NULL)",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "93def20ecf1bcbffe57c54bb12f0a5da22d9d2845d69e998cad8b68954b7a289"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE folder SET display_name = 'Modified Shared Folder'\n WHERE workspace_id = 'wm-fork-test-workspace' AND name = 'shared'",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "96406f7d7a42ec5a614415ed2fbf6f24291e869aa028ef3190390aaeadd10a46"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO script (workspace_id, path, hash, content, summary, description, language, created_by, created_at, archived, schema_validation, ws_error_handler_muted, deleted)\n VALUES ('wm-fork-test-workspace', 'f/shared/new_in_fork', 99999, 'def main(): return \"fork\"', 'New in fork', '', 'python3', 'test@windmill.dev', NOW(), false, false, false, false)",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "9775cbe9f8592743575b06ae60b4afdca78d46a5934521314bb7a7d8688f9a35"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO app (workspace_id, path, summary, policy, versions, extra_perms, draft_only)\n VALUES ('test-workspace', 'f/shared/dashboard', 'Dashboard app', '{}', ARRAY[1::bigint], '{}', false)",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "9a7f4786fc29ed2b561d9eb96c274c66da29fdfb3e862e06c10c5deb4a2b5771"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE resource SET path = 'f/shared/new_name'\n WHERE workspace_id = 'test-workspace' AND path = 'f/shared/old_name'",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "9b7d0f980267e62fb50545570e83daae65e4458a88b53b65870523b64d183231"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE variable SET value = 'modified_value'\n WHERE workspace_id = 'test-workspace' AND path = 'f/shared/variable_to_modify'",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "9df747dec3f3c245b1cd3a5039f0a4a69d0667e328ad838c1a2f9bb9fee2d121"
}

View File

@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO resource (workspace_id, path, value, resource_type, description, created_by)\n VALUES\n ('test-workspace', 'f/shared/db_config', $1, 'postgresql', '', 'test@windmill.dev'),\n ('test-workspace', 'f/shared/old_name', $2, 'generic', '', 'test@windmill.dev'),\n ('test-workspace', 'f/shared/resource_to_modify', $3, 'generic', '', 'test@windmill.dev')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Jsonb",
"Jsonb"
]
},
"nullable": []
},
"hash": "a17539823e5f2b0ffa8d3f270801d6d41db9e2ef4c33f09565a858d6c55cc3a9"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_diff\n (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)\n VALUES\n ('test-workspace', 'wm-fork-test-workspace', 'f/shared/original_flow', 'flow', 1, 1, NULL),\n ('test-workspace', 'wm-fork-test-workspace', 'f/shared/to_conflict', 'script', 1, 1, NULL)",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "a1f8d5c97404166603de2fe214d965243ddf8a37685a7f62b593718d195dab06"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO script (workspace_id, path, hash, content, summary, description, language, created_by, created_at, archived, schema_validation, ws_error_handler_muted, deleted)\n VALUES ('test-workspace', 'f/shared/new_in_parent', 54321, 'def main(): return \"new\"', 'New in parent', '', 'python3', 'test@windmill.dev', NOW(), false, false, false, false)",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "a962567ecfcbf251a8fcc12db894580b53f209d5f9f771bfaf0453b8d704bf31"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO flow (workspace_id, path, summary, description, value, schema, edited_by, edited_at, archived)\n VALUES ('test-workspace', 'f/shared/original_flow', 'Flow summary', '', $1, NULL, 'test@windmill.dev', NOW(), false)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb"
]
},
"nullable": []
},
"hash": "b32c7c24440137d6749e1fc3095512b0f5bc8b81e0b644b01256398664ecf9b1"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT name FROM resource_type\n WHERE workspace_id = $1 AND name = ANY($2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"TextArray"
]
},
"nullable": [
false
]
},
"hash": "b95b6ee787c590dd21969965a687fef33fc9fbc80ca87f96aced906570e28c18"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO folder (workspace_id, name, display_name, owners, summary, created_by)\n VALUES ('test-workspace', 'shared', 'Shared Folder', ARRAY['test@windmill.dev']::varchar[], 'Test folder', 'test@windmill.dev')",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "ba3149ec0aa62dba41c459a1bf5be86aebccee68172abefe82f4bb76fc7a80ee"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO app_version (app_id, value, created_by, created_at)\n VALUES ($1, $2, 'test@windmill.dev', NOW())",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Json"
]
},
"nullable": []
},
"hash": "bda182047ae37e263b0011248d86c1b20d8b7908bb8a8ae4c5ed83b51db5446a"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_diff\n (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)\n VALUES\n ('test-workspace', 'wm-fork-test-workspace', 'f/shared/to_modify_parent', 'script', 1, 0, NULL),\n ('test-workspace', 'wm-fork-test-workspace', 'f/shared/dashboard', 'app', 1, 0, NULL),\n ('test-workspace', 'wm-fork-test-workspace', 'f/shared/variable_to_modify', 'variable', 1, 0, NULL)",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "c925a52d90231f9e6f3af652ddfddd67bff8c3d306794e3583af86818b7054d0"
}

View File

@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO agent_token_blacklist (token, expires_at, blacklisted_by) \n VALUES ($1, $2, $3) \n ON CONFLICT (token) DO UPDATE SET \n expires_at = EXCLUDED.expires_at,\n blacklisted_at = NOW(),\n blacklisted_by = EXCLUDED.blacklisted_by",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Timestamp",
"Varchar"
]
},
"nullable": []
},
"hash": "c9c040ec228a8fe4fda08439420141bee63339d4e3d5e2d68aabb12009f691c6"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT token, expires_at, blacklisted_at, blacklisted_by\n FROM agent_token_blacklist\n WHERE expires_at > $1\n ORDER BY blacklisted_at DESC",
"query": "SELECT token, expires_at, blacklisted_at, blacklisted_by \n FROM agent_token_blacklist \n WHERE expires_at > $1 \n ORDER BY blacklisted_at DESC",
"describe": {
"columns": [
{
@@ -36,5 +36,5 @@
false
]
},
"hash": "d20d6c43b16b762fb4cdb2cafe1fe9a2920124f398fca5bd54cb805b03b94763"
"hash": "d56722c25877222af9affd5da5bb83b28fa8cbb528a2cfc90684cb10a69e4375"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE app SET summary = 'Modified dashboard app'\n WHERE workspace_id = 'test-workspace' AND path = 'f/shared/dashboard'",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "d6ce9016308da0f20c2118c047349d8110d899121c9c00795c55217b9f1886c2"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO variable (workspace_id, path, value, is_secret, description)\n VALUES\n ('test-workspace', 'f/shared/api_key', 'secret123', false, 'Test key'),\n ('test-workspace', 'f/shared/variable_to_modify', 'original', false, 'To modify')",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "d8c5c64006cdd52570bbc56b0fa5df9b597d9a45353391411efa86959a7512bc"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT name FROM folder\n WHERE workspace_id = $1 AND name = ANY($2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"TextArray"
]
},
"nullable": [
false
]
},
"hash": "df804e2a8795af329aa84ccfeb90329383524232276908af181f6ecb3c8fc804"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE flow SET value = $1\n WHERE workspace_id = 'wm-fork-test-workspace' AND path = 'f/shared/original_flow'",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb"
]
},
"nullable": []
},
"hash": "e42bb9155be93cbc750702cf7f9a3c54d1a236421861880fe3749052bd2ab43f"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_diff\n (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)\n VALUES ('test-workspace', 'wm-fork-test-workspace', 'f/shared/to_delete', 'script', 1, 0, NULL)",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "e881a777b8e67c76cf458828a02f7d5234ae76da567195f5da11cd11d7ebbc05"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE resource SET value = $1\n WHERE workspace_id = 'wm-fork-test-workspace' AND path = 'f/shared/resource_to_modify'",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb"
]
},
"nullable": []
},
"hash": "e9e6c8ba034b4f80fada2e07e500a78983aeb1a7b1185b5267229c3197d6f714"
}

View File

@@ -11,11 +11,10 @@ Windmill uses a workspace-based architecture with multiple crates:
- **windmill-audit**: Audit logging
- Other specialized crates (git-sync, autoscaling, etc.)
## Key References (MUST FOLLOW THESE)
## Key References
- You MUST follow best-practices by using the `rust-backend` skill, everytime you write RUST code.
- When working with the database: read `summarized_schema.txt` before starting
- When working with the API routes: you can read `windmill-api/src/lib.rs` to get started
- Database schema: @summarized_schema.txt
- API route prefixes: `windmill-api/src/lib.rs`
## Adding New Code
@@ -58,4 +57,8 @@ Windmill uses a workspace-based architecture with multiple crates:
- **sqlx**: Database operations
- **serde**: Serialization/deserialization
- **tracing**: Logging and diagnostics
- **reqwest**: HTTP client
- **reqwest**: HTTP client
## Coding Guidelines
Detailed Rust coding patterns and best practices are provided by the `rust-backend` skill.

114
backend/Cargo.lock generated
View File

@@ -1117,9 +1117,9 @@ dependencies = [
[[package]]
name = "aws-smithy-async"
version = "1.2.10"
version = "1.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e39f47abe8641e434de98e047e85ced629862e7ab719b6914a846796ceb289e2"
checksum = "9330762ee48c6cecfad2cb37b1506c16c8e858c90638eda2b1a7272b56f88bd5"
dependencies = [
"futures-util",
"pin-project-lite",
@@ -1128,9 +1128,9 @@ dependencies = [
[[package]]
name = "aws-smithy-eventstream"
version = "0.60.17"
version = "0.60.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff6a1db93c9aaf8c8e5d97f055e5fa01a782bd5bdc9c4042b7e18090503b12a7"
checksum = "0810b22ae554f5076c3eabe1fe89b01aee61c354c575789f67e248e83c5f472b"
dependencies = [
"aws-smithy-types",
"bytes",
@@ -1161,9 +1161,9 @@ dependencies = [
[[package]]
name = "aws-smithy-http-client"
version = "1.1.8"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a395c914b1ff95db3cb7003ea4fd19432343af698a9b5028a7d35f8e712240a1"
checksum = "ec918f18147cec121cb142a91b0038f66d99bbe903e585dccf871920e90b22ab"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
@@ -1200,18 +1200,18 @@ dependencies = [
[[package]]
name = "aws-smithy-observability"
version = "0.2.3"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7764bc1dfdb71157bc481528a649e617ed8c9c8aa93c0e8b01087133677cfc8e"
checksum = "a700a7702874cd78b85fecdc9f64f3f72eb22fb713791cb445bcfd2a15bc1ecf"
dependencies = [
"aws-smithy-runtime-api",
]
[[package]]
name = "aws-smithy-query"
version = "0.60.12"
version = "0.60.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "786bbf4434ed3e3413c5a1741abf53a0372dd48124ddb2091e6bff1b1b2582b5"
checksum = "adc4a6cdc289a37be7fddb7f4365448187d62c603a40e6d46d13c68e5e81900f"
dependencies = [
"aws-smithy-types",
"urlencoding",
@@ -1243,9 +1243,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime-api"
version = "1.11.2"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b743e9aab0b8d50a9a40eebedf974fcfe3621032e07c6388d1c7821b155b7b0"
checksum = "5c47b1e62accf759b01aba295e40479d1ba8fb77c2a54f0fed861c809ca49761"
dependencies = [
"aws-smithy-async",
"aws-smithy-types",
@@ -1260,9 +1260,9 @@ dependencies = [
[[package]]
name = "aws-smithy-types"
version = "1.4.2"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0828575b70da70406b4cdb5d4afe0afe725f72245f04d34f02e0fb5ebd6fc872"
checksum = "c2d447863bdec38c899e5753a48c0abcf590f3ec629e257ad5a9ef8806ad7714"
dependencies = [
"base64-simd 0.8.0",
"bytes",
@@ -1976,9 +1976,9 @@ dependencies = [
[[package]]
name = "bytemuck"
version = "1.25.0"
version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
dependencies = [
"bytemuck_derive",
]
@@ -2150,9 +2150,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.55"
version = "1.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -5566,9 +5566,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
[[package]]
name = "fixedbitset"
@@ -10054,9 +10054,9 @@ dependencies = [
[[package]]
name = "portable-atomic"
version = "1.13.1"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950"
[[package]]
name = "postgres-native-tls"
@@ -12399,9 +12399,9 @@ dependencies = [
[[package]]
name = "slab"
version = "0.4.12"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
[[package]]
name = "slotmap"
@@ -15466,7 +15466,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"aws-sdk-config",
@@ -15529,7 +15529,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"argon2",
@@ -15659,7 +15659,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -15669,7 +15669,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"chrono",
"lazy_static",
@@ -15683,7 +15683,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -15702,7 +15702,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"async-recursion",
@@ -15798,7 +15798,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"regex",
"serde",
@@ -15813,7 +15813,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -15837,7 +15837,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -15853,7 +15853,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"async-trait",
@@ -15873,7 +15873,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"async-oauth2",
@@ -15897,7 +15897,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -15906,7 +15906,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -15918,7 +15918,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"serde_json",
@@ -15930,7 +15930,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"gosyn",
@@ -15942,7 +15942,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -15954,7 +15954,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"serde_json",
@@ -15966,7 +15966,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"nu-parser",
@@ -15977,7 +15977,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15988,7 +15988,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16001,7 +16001,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"async-recursion",
@@ -16025,7 +16025,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -16039,7 +16039,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -16056,7 +16056,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -16070,7 +16070,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -16089,7 +16089,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"serde",
@@ -16100,7 +16100,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"async-recursion",
@@ -16137,7 +16137,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -16147,7 +16147,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.623.0"
version = "1.621.2"
dependencies = [
"anyhow",
"async-once-cell",
@@ -17048,18 +17048,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.37"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac"
checksum = "fdea86ddd5568519879b8187e1cf04e24fce28f7fe046ceecbce472ff19a2572"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.37"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0"
checksum = "0c15e1b46eff7c6c91195752e0eeed8ef040e391cdece7c25376957d5f15df22"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.623.0"
version = "1.621.2"
authors.workspace = true
edition.workspace = true
@@ -35,7 +35,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.623.0"
version = "1.621.2"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"

View File

@@ -1 +1 @@
fa881b63272aebab8ef79d262be7da2a2908c227
a18ac31062ac092cb9a5fc87629e217d97f4911d

View File

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

View File

@@ -1,2 +0,0 @@
-- Add up migration script here
UPDATE workspace_diff SET has_changes = NULL;

View File

@@ -34,7 +34,7 @@ use windmill_common::ee_oss::{
};
use windmill_common::{
agent_workers::AgentConfig,
agent_workers::build_agent_http_client,
global_settings::{
APP_WORKSPACED_ROUTE_SETTING, BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING,
CRITICAL_ALERTS_ON_DB_OVERSIZE_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING,
@@ -43,14 +43,13 @@ use windmill_common::{
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INDEXER_SETTING,
INSTANCE_PYTHON_VERSION_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING,
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MAVEN_REPOS_SETTING,
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MAVEN_REPOS_SETTING, OTEL_TRACING_PROXY_SETTING,
MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NO_DEFAULT_MAVEN_SETTING,
NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OAUTH_SETTING, OTEL_SETTING,
OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING,
POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
RUBY_REPOS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING, TEAMS_SETTING,
TIMEOUT_WAIT_RESULT_SETTING,
PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING, POWERSHELL_REPO_URL_SETTING,
REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING,
RETENTION_PERIOD_SECS_SETTING, RUBY_REPOS_SETTING, SAML_METADATA_SETTING,
SCIM_TOKEN_SETTING, SMTP_SETTING, TEAMS_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
},
scripts::ScriptLang,
stats_oss::schedule_stats,
@@ -100,10 +99,9 @@ use crate::monitor::{
reload_bunfig_install_scopes_setting, reload_critical_alert_mute_ui_setting,
reload_critical_error_channels_setting, reload_extra_pip_index_url_setting,
reload_hub_api_secret_setting, reload_hub_base_url_setting, reload_job_default_timeout_setting,
reload_jwt_secret_setting, reload_license_key, reload_npm_config_registry_setting,
reload_otel_tracing_proxy_setting, reload_pip_index_url_setting,
reload_retention_period_setting, reload_scim_token_setting, reload_smtp_config,
reload_worker_config, MonitorIteration,
reload_jwt_secret_setting, reload_license_key, reload_otel_tracing_proxy_setting,
reload_npm_config_registry_setting, reload_pip_index_url_setting, reload_retention_period_setting,
reload_scim_token_setting, reload_smtp_config, reload_worker_config, MonitorIteration,
};
#[cfg(feature = "parquet")]
@@ -412,10 +410,7 @@ pub async fn sync_cached_resource_types(db: &sqlx::Pool<sqlx::Postgres>) -> anyh
let cache_path = format!("{}/{}", HUB_RT_CACHE_DIR, HUB_RT_CACHE_FILE);
if tokio::fs::metadata(&cache_path).await.is_err() {
tracing::info!(
"No cached resource types found at {}, skipping sync",
cache_path
);
tracing::info!("No cached resource types found at {}, skipping sync", cache_path);
return Ok(());
}
@@ -425,8 +420,8 @@ pub async fn sync_cached_resource_types(db: &sqlx::Pool<sqlx::Postgres>) -> anyh
.await
.with_context(|| format!("Failed to read cache file from {}", cache_path))?;
let cached_types: Vec<HubResourceType> =
serde_json::from_str(&content).with_context(|| "Failed to parse cached resource types")?;
let cached_types: Vec<HubResourceType> = serde_json::from_str(&content)
.with_context(|| "Failed to parse cached resource types")?;
tracing::info!("Found {} cached resource types", cached_types.len());
@@ -438,13 +433,11 @@ pub async fn sync_cached_resource_types(db: &sqlx::Pool<sqlx::Postgres>) -> anyh
.await
.with_context(|| "Failed to fetch existing resource types")?;
let existing_map: std::collections::HashMap<
String,
(Option<serde_json::Value>, Option<String>),
> = existing_types
.into_iter()
.map(|(name, schema, desc)| (name, (schema, desc)))
.collect();
let existing_map: std::collections::HashMap<String, (Option<serde_json::Value>, Option<String>)> =
existing_types
.into_iter()
.map(|(name, schema, desc)| (name, (schema, desc)))
.collect();
let mut synced_count = 0;
let mut skipped_count = 0;
@@ -485,45 +478,36 @@ pub async fn sync_cached_resource_types(db: &sqlx::Pool<sqlx::Postgres>) -> anyh
}
fn print_help() {
println!("Windmill - a fast, open-source workflow engine and job runner.");
println!();
println!("Usage:");
println!(" windmill [SUBCOMMAND]");
println!();
println!("Subcommands:");
println!(" help | -h | --help Show this help information and exit");
println!(" version Show Windmill version and exit");
println!(" cache [hubPaths.json] Pre-cache hub scripts (default: ./hubPaths.json)");
println!(" cache-rt Pre-cache hub resource types");
println!();
println!("Environment variables (name = default):");
println!(" DATABASE_URL = <required> The Postgres database url.");
println!(" MODE = standalone Mode: standalone | worker | server | agent");
println!(" BASE_URL = http://localhost:8000 Public base URL of your instance (overridden by instance settings)");
println!(
" PORT = {} HTTP port (server/indexer/MCP modes)",
DEFAULT_PORT
);
println!(
" SERVER_BIND_ADDR = <mode dependent> IP to bind to (server: {}, worker: {})",
DEFAULT_SERVER_BIND_ADDR, DEFAULT_WORKER_BIND_ADDR
);
println!(
" NUM_WORKERS = {} Number of workers (standalone/worker modes)",
DEFAULT_NUM_WORKERS
);
println!(" WORKER_GROUP = default Worker group this worker belongs to",);
println!(" JSON_FMT = false Output logs in JSON instead of logfmt");
println!(" METRICS_ADDR = None (EE only) Prometheus metrics addr at /metrics; set \"true\" to use :8001");
println!(" SUPERADMIN_SECRET = None Virtual superadmin token (server)");
println!(" LICENSE_KEY = None (EE only) Enterprise license key (workers require valid key)");
println!(" RUN_UPDATE_CA_CERTIFICATE_AT_START = false Run system CA update at startup");
println!(" RUN_UPDATE_CA_CERTIFICATE_PATH = /usr/sbin/update-ca-certificates Path to CA update tool");
println!(" SYNC_CACHED_RT = false Sync cached resource types to admins workspace on server start");
println!();
println!("Notes:");
println!("- Advanced and less commonly used settings are managed via the database and are omitted here.");
println!("- At startup, Windmill logs currently set configuration keys for visibility.");
println!("Windmill - a fast, open-source workflow engine and job runner.");
println!();
println!("Usage:");
println!(" windmill [SUBCOMMAND]");
println!();
println!("Subcommands:");
println!(" help | -h | --help Show this help information and exit");
println!(" version Show Windmill version and exit");
println!(" cache [hubPaths.json] Pre-cache hub scripts (default: ./hubPaths.json)");
println!(" cache-rt Pre-cache hub resource types");
println!();
println!("Environment variables (name = default):");
println!(" DATABASE_URL = <required> The Postgres database url.");
println!(" MODE = standalone Mode: standalone | worker | server | agent");
println!(" BASE_URL = http://localhost:8000 Public base URL of your instance (overridden by instance settings)");
println!(" PORT = {} HTTP port (server/indexer/MCP modes)", DEFAULT_PORT);
println!(" SERVER_BIND_ADDR = <mode dependent> IP to bind to (server: {}, worker: {})", DEFAULT_SERVER_BIND_ADDR, DEFAULT_WORKER_BIND_ADDR);
println!(" NUM_WORKERS = {} Number of workers (standalone/worker modes)", DEFAULT_NUM_WORKERS);
println!(" WORKER_GROUP = default Worker group this worker belongs to",);
println!(" JSON_FMT = false Output logs in JSON instead of logfmt");
println!(" METRICS_ADDR = None (EE only) Prometheus metrics addr at /metrics; set \"true\" to use :8001");
println!(" SUPERADMIN_SECRET = None Virtual superadmin token (server)");
println!(" LICENSE_KEY = None (EE only) Enterprise license key (workers require valid key)");
println!(" RUN_UPDATE_CA_CERTIFICATE_AT_START = false Run system CA update at startup");
println!(" RUN_UPDATE_CA_CERTIFICATE_PATH = /usr/sbin/update-ca-certificates Path to CA update tool");
println!(" SYNC_CACHED_RT = false Sync cached resource types to admins workspace on server start");
println!();
println!("Notes:");
println!("- Advanced and less commonly used settings are managed via the database and are omitted here.");
println!("- At startup, Windmill logs currently set configuration keys for visibility.");
}
async fn windmill_main() -> anyhow::Result<()> {
@@ -657,23 +641,15 @@ async fn windmill_main() -> anyhow::Result<()> {
.and_then(|x| x.parse().ok())
.unwrap_or(IpAddr::from(default_bind_addr));
let (conn, first_suffix, agent_config) = if mode == Mode::Agent {
let agent_config = match AgentConfig::from_env() {
Ok(config) => config,
Err(e) => {
tracing::error!("{e}");
std::process::exit(1);
}
};
let (conn, first_suffix) = if mode == Mode::Agent {
tracing::info!(
"Creating http client for cluster using base internal url {}",
agent_config.base_internal_url
std::env::var("BASE_INTERNAL_URL").unwrap_or_default()
);
let suffix = create_default_worker_suffix(&hostname);
(
Connection::Http(agent_config.build_http_client(&suffix)),
Connection::Http(build_agent_http_client(&suffix, None, None)),
Some(suffix),
Some(agent_config),
)
} else {
println!("Connecting to database...");
@@ -697,8 +673,7 @@ async fn windmill_main() -> anyhow::Result<()> {
reload_otel_tracing_proxy_setting(&Connection::Sql(db.clone())).await;
#[cfg(feature = "deno_core")]
if windmill_worker::is_otel_tracing_proxy_enabled_for_lang(&ScriptLang::Nativets).await
{
if windmill_worker::is_otel_tracing_proxy_enabled_for_lang(&ScriptLang::Nativets).await {
match windmill_worker::load_internal_otel_exporter().await {
Ok(()) => {
tracing::info!("Internal OTEL exporter initialized for nativets tracing");
@@ -713,7 +688,7 @@ async fn windmill_main() -> anyhow::Result<()> {
load_otel(&db).await;
println!("Database connected");
(Connection::Sql(db), None, None)
(Connection::Sql(db), None)
};
let environment = if let Ok(environment) = std::env::var("OTEL_ENVIRONMENT") {
@@ -824,12 +799,6 @@ Windmill Community Edition {GIT_VERSION}
// if key still invalid and num_workers > 0, set to 0
if let Err(err) = reload_license_key(&conn).await {
tracing::error!("Failed to reload license key: {err:#}");
if is_agent {
tracing::error!(
"Agent worker cannot connect to server. Please check AGENT_TOKEN and BASE_INTERNAL_URL"
);
std::process::exit(1);
}
}
let valid_key = *LICENSE_KEY_VALID.read().await;
if !valid_key && !server_mode {
@@ -1088,12 +1057,7 @@ Windmill Community Edition {GIT_VERSION}
conn: if i == 0 || mode != Mode::Agent {
conn.clone()
} else {
Connection::Http(
agent_config
.as_ref()
.expect("agent_config must be set in agent mode")
.build_http_client(&suffix),
)
Connection::Http(build_agent_http_client(&suffix, None, None))
},
worker_name: worker_name_with_suffix(
mode == Mode::Agent,

View File

@@ -764,25 +764,23 @@ pub async fn run_preview_relative_imports(
#[cfg(all(feature = "private", feature = "agent_worker_server"))]
pub async fn testing_http_connection(port: u16) -> Connection {
let suffix = windmill_common::utils::create_default_worker_suffix("test-agent-worker");
let agent_token = format!(
"{}{}",
windmill_common::agent_workers::AGENT_JWT_PREFIX,
windmill_common::jwt::encode_with_internal_secret(
windmill_api::agent_workers_ee::AgentAuth {
worker_group: "testing-agent".to_owned(),
suffix: Some(suffix.clone()),
tags: vec!["flow".into(), "python3".into(), "dependency".into()],
exp: Some(usize::MAX),
}
)
.await
.expect("JWT token to be created")
);
let base_internal_url = format!("http://localhost:{port}");
Connection::Http(windmill_common::agent_workers::build_agent_http_client(
&suffix,
&agent_token,
&base_internal_url,
Some(format!(
"{}{}",
windmill_common::agent_workers::AGENT_JWT_PREFIX,
windmill_common::jwt::encode_with_internal_secret(
windmill_api::agent_workers_ee::AgentAuth {
worker_group: "testing-agent".to_owned(),
suffix: Some(suffix.clone()),
tags: vec!["flow".into(), "python3".into(), "dependency".into()],
exp: Some(usize::MAX),
}
)
.await
.expect("JWT token to be created")
)),
Some(format!("http://localhost:{port}")),
))
}

View File

@@ -1,507 +0,0 @@
use serde_json::json;
use sqlx::{Pool, Postgres};
mod common;
use common::*;
/// Comprehensive integration test for the compare_workspaces endpoint.
///
/// This test validates workspace fork comparison functionality by:
/// 1. Setting up a parent workspace with all item types (scripts, flows, apps, resources, variables, resource_types, folders)
/// 2. Creating a fork of the workspace
/// 3. Making various changes in both workspaces (new items, modifications, conflicts, deletions, renames)
/// 4. Populating the workspace_diff table to simulate Git sync tracking
/// 5. Calling compare_workspaces and verifying all aspects of the comparison
#[sqlx::test(fixtures("base"))]
async fn test_compare_workspaces_comprehensive(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let client = windmill_api_client::create_client(
&format!("http://localhost:{port}"),
"SECRET_TOKEN".to_string(),
);
let base_url = format!("http://localhost:{port}/api");
// ==============================================================
// PHASE 1: Setup Parent Workspace with All Item Types
// ==============================================================
// Create folder first (other items will use it)
sqlx::query!(
"INSERT INTO folder (workspace_id, name, display_name, owners, summary, created_by)
VALUES ('test-workspace', 'shared', 'Shared Folder', ARRAY['test@windmill.dev']::varchar[], 'Test folder', 'test@windmill.dev')"
)
.execute(&db)
.await?;
// Create scripts
sqlx::query!(
"INSERT INTO script (workspace_id, path, hash, content, summary, description, language, created_by, created_at, archived, schema_validation, ws_error_handler_muted, deleted)
VALUES
('test-workspace', 'f/shared/original_script', 12345, 'def main(): pass', 'Original', '', 'python3', 'test@windmill.dev', NOW(), false, false, false, false),
('test-workspace', 'f/shared/to_modify_parent', 22222, 'def main(): return 1', 'To modify in parent', '', 'python3', 'test@windmill.dev', NOW(), false, false, false, false),
('test-workspace', 'f/shared/to_modify_fork', 33333, 'def main(): return 2', 'To modify in fork', '', 'python3', 'test@windmill.dev', NOW(), false, false, false, false),
('test-workspace', 'f/shared/to_conflict', 44444, 'def main(): return 3', 'To conflict', '', 'python3', 'test@windmill.dev', NOW(), false, false, false, false),
('test-workspace', 'f/shared/to_delete', 55555, 'def main(): return 4', 'To delete', '', 'python3', 'test@windmill.dev', NOW(), false, false, false, false)"
)
.execute(&db)
.await?;
// Create flow
sqlx::query!(
"INSERT INTO flow (workspace_id, path, summary, description, value, schema, edited_by, edited_at, archived)
VALUES ('test-workspace', 'f/shared/original_flow', 'Flow summary', '', $1, NULL, 'test@windmill.dev', NOW(), false)",
json!({"modules": []})
)
.execute(&db)
.await?;
// Create resource
sqlx::query!(
"INSERT INTO resource (workspace_id, path, value, resource_type, description, created_by)
VALUES
('test-workspace', 'f/shared/db_config', $1, 'postgresql', '', 'test@windmill.dev'),
('test-workspace', 'f/shared/old_name', $2, 'generic', '', 'test@windmill.dev'),
('test-workspace', 'f/shared/resource_to_modify', $3, 'generic', '', 'test@windmill.dev')",
json!({"host": "localhost"}),
json!({}),
json!({"key": "value"})
)
.execute(&db)
.await?;
// Create variable
sqlx::query!(
"INSERT INTO variable (workspace_id, path, value, is_secret, description)
VALUES
('test-workspace', 'f/shared/api_key', 'secret123', false, 'Test key'),
('test-workspace', 'f/shared/variable_to_modify', 'original', false, 'To modify')"
)
.execute(&db)
.await?;
// Create resource type
sqlx::query!(
"INSERT INTO resource_type (workspace_id, name, schema, description, created_by)
VALUES ('test-workspace', 'custom_db', $1, 'Custom DB type', 'test@windmill.dev')",
json!({"type": "object"})
)
.execute(&db)
.await?;
// Create app
sqlx::query!(
"INSERT INTO app (workspace_id, path, summary, policy, versions, extra_perms, draft_only)
VALUES ('test-workspace', 'f/shared/dashboard', 'Dashboard app', '{}', ARRAY[1::bigint], '{}', false)"
)
.execute(&db)
.await?;
let app_id = sqlx::query_scalar!(
"SELECT id FROM app WHERE path = 'f/shared/dashboard' AND workspace_id = 'test-workspace'"
)
.fetch_one(&db)
.await?;
sqlx::query!(
"INSERT INTO app_version (app_id, value, created_by, created_at)
VALUES ($1, $2, 'test@windmill.dev', NOW())",
app_id,
json!({"grid": []})
)
.execute(&db)
.await?;
// ==============================================================
// PHASE 2: Create Fork
// ==============================================================
let fork_response = client
.client()
.post(&format!("{base_url}/w/test-workspace/workspaces/create_fork"))
.json(&json!({
"id": "wm-fork-test-workspace",
"name": "Test Fork",
"color": "#0000ff"
}))
.send()
.await?;
let status = fork_response.status();
assert!(status.is_success(), "Fork creation should succeed: {}", status);
// Verify fork was created
let fork_exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM workspace WHERE id = 'wm-fork-test-workspace')"
)
.fetch_one(&db)
.await?;
assert!(fork_exists.unwrap_or(false), "Fork workspace should exist");
// ==============================================================
// PHASE 3: Make Changes in Both Workspaces
// ==============================================================
// Scenario 1: New script in parent (ahead)
sqlx::query!(
"INSERT INTO script (workspace_id, path, hash, content, summary, description, language, created_by, created_at, archived, schema_validation, ws_error_handler_muted, deleted)
VALUES ('test-workspace', 'f/shared/new_in_parent', 54321, 'def main(): return \"new\"', 'New in parent', '', 'python3', 'test@windmill.dev', NOW(), false, false, false, false)"
)
.execute(&db)
.await?;
// Scenario 2: New script in fork (behind)
sqlx::query!(
"INSERT INTO script (workspace_id, path, hash, content, summary, description, language, created_by, created_at, archived, schema_validation, ws_error_handler_muted, deleted)
VALUES ('wm-fork-test-workspace', 'f/shared/new_in_fork', 99999, 'def main(): return \"fork\"', 'New in fork', '', 'python3', 'test@windmill.dev', NOW(), false, false, false, false)"
)
.execute(&db)
.await?;
// Scenario 3: Modify script in parent (ahead)
sqlx::query!(
"UPDATE script
SET content = 'def main(): return \"modified\"', summary = 'Modified in parent'
WHERE workspace_id = 'test-workspace' AND path = 'f/shared/to_modify_parent'"
)
.execute(&db)
.await?;
// Scenario 4: Modify script in fork (behind)
sqlx::query!(
"UPDATE script
SET content = 'def main(): return \"fork_modified\"', summary = 'Modified in fork'
WHERE workspace_id = 'wm-fork-test-workspace' AND path = 'f/shared/to_modify_fork'"
)
.execute(&db)
.await?;
// Scenario 5: Conflict - modify in both workspaces
sqlx::query!(
"UPDATE flow SET value = $1
WHERE workspace_id = 'test-workspace' AND path = 'f/shared/original_flow'",
json!({"modules": [{"id": "a"}]})
)
.execute(&db)
.await?;
sqlx::query!(
"UPDATE flow SET value = $1
WHERE workspace_id = 'wm-fork-test-workspace' AND path = 'f/shared/original_flow'",
json!({"modules": [{"id": "b"}]})
)
.execute(&db)
.await?;
// Scenario 6: Delete (archive) in fork
sqlx::query!(
"UPDATE script SET archived = true
WHERE workspace_id = 'wm-fork-test-workspace' AND path = 'f/shared/to_delete'"
)
.execute(&db)
.await?;
// Scenario 7: Rename in parent (resource)
sqlx::query!(
"UPDATE resource SET path = 'f/shared/new_name'
WHERE workspace_id = 'test-workspace' AND path = 'f/shared/old_name'"
)
.execute(&db)
.await?;
// Scenario 8: Modify app in parent
sqlx::query!(
"UPDATE app SET summary = 'Modified dashboard app'
WHERE workspace_id = 'test-workspace' AND path = 'f/shared/dashboard'"
)
.execute(&db)
.await?;
// Scenario 9: Modify resource in fork
sqlx::query!(
"UPDATE resource SET value = $1
WHERE workspace_id = 'wm-fork-test-workspace' AND path = 'f/shared/resource_to_modify'",
json!({"key": "modified_value"})
)
.execute(&db)
.await?;
// Modify variable in parent
sqlx::query!(
"UPDATE variable SET value = 'modified_value'
WHERE workspace_id = 'test-workspace' AND path = 'f/shared/variable_to_modify'"
)
.execute(&db)
.await?;
// Create new resource type in parent
sqlx::query!(
"INSERT INTO resource_type (workspace_id, name, schema, description, created_by)
VALUES ('test-workspace', 'new_type', $1, 'New type in parent', 'test@windmill.dev')",
json!({"type": "string"})
)
.execute(&db)
.await?;
// Modify folder in fork (display_name)
sqlx::query!(
"UPDATE folder SET display_name = 'Modified Shared Folder'
WHERE workspace_id = 'wm-fork-test-workspace' AND name = 'shared'"
)
.execute(&db)
.await?;
// ==============================================================
// PHASE 4: Populate workspace_diff Table
// ==============================================================
// New in parent (ahead)
sqlx::query!(
"INSERT INTO workspace_diff
(source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)
VALUES
('test-workspace', 'wm-fork-test-workspace', 'f/shared/new_in_parent', 'script', 1, 0, NULL),
('test-workspace', 'wm-fork-test-workspace', 'new_type', 'resource_type', 1, 0, NULL)"
)
.execute(&db)
.await?;
// New in fork (behind)
sqlx::query!(
"INSERT INTO workspace_diff
(source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)
VALUES ('test-workspace', 'wm-fork-test-workspace', 'f/shared/new_in_fork', 'script', 0, 1, NULL)"
)
.execute(&db)
.await?;
// Modified in parent (ahead)
sqlx::query!(
"INSERT INTO workspace_diff
(source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)
VALUES
('test-workspace', 'wm-fork-test-workspace', 'f/shared/to_modify_parent', 'script', 1, 0, NULL),
('test-workspace', 'wm-fork-test-workspace', 'f/shared/dashboard', 'app', 1, 0, NULL),
('test-workspace', 'wm-fork-test-workspace', 'f/shared/variable_to_modify', 'variable', 1, 0, NULL)"
)
.execute(&db)
.await?;
// Modified in fork (behind)
sqlx::query!(
"INSERT INTO workspace_diff
(source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)
VALUES
('test-workspace', 'wm-fork-test-workspace', 'f/shared/to_modify_fork', 'script', 0, 1, NULL),
('test-workspace', 'wm-fork-test-workspace', 'f/shared/resource_to_modify', 'resource', 0, 1, NULL),
('test-workspace', 'wm-fork-test-workspace', 'shared', 'folder', 0, 1, NULL)"
)
.execute(&db)
.await?;
// Conflict (both ahead and behind)
sqlx::query!(
"INSERT INTO workspace_diff
(source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)
VALUES
('test-workspace', 'wm-fork-test-workspace', 'f/shared/original_flow', 'flow', 1, 1, NULL),
('test-workspace', 'wm-fork-test-workspace', 'f/shared/to_conflict', 'script', 1, 1, NULL)"
)
.execute(&db)
.await?;
// Deleted in fork (exists only in parent)
sqlx::query!(
"INSERT INTO workspace_diff
(source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)
VALUES ('test-workspace', 'wm-fork-test-workspace', 'f/shared/to_delete', 'script', 1, 0, NULL)"
)
.execute(&db)
.await?;
// Renamed in parent (two entries)
sqlx::query!(
"INSERT INTO workspace_diff
(source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)
VALUES
('test-workspace', 'wm-fork-test-workspace', 'f/shared/old_name', 'resource', 0, 1, NULL),
('test-workspace', 'wm-fork-test-workspace', 'f/shared/new_name', 'resource', 1, 0, NULL)"
)
.execute(&db)
.await?;
// Add an unchanged item to verify it gets filtered out
sqlx::query!(
"INSERT INTO workspace_diff
(source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)
VALUES ('test-workspace', 'wm-fork-test-workspace', 'f/shared/original_script', 'script', 0, 0, NULL)"
)
.execute(&db)
.await?;
// ==============================================================
// PHASE 5: Call compare_workspaces and Verify Results
// ==============================================================
let comparison: serde_json::Value = client
.client()
.get(&format!("{base_url}/w/test-workspace/workspaces/compare/wm-fork-test-workspace"))
.send()
.await?
.json()
.await?;
// Verify basic structure
assert!(!comparison["skipped_comparison"].as_bool().unwrap_or(true), "Should not skip comparison");
assert!(comparison["diffs"].is_array(), "Should have diffs array");
assert!(comparison["summary"].is_object(), "Should have summary object");
let diffs = comparison["diffs"].as_array().unwrap();
let summary = &comparison["summary"];
// ==============================================================
// Summary Assertions
// ==============================================================
// Total diffs (excluding unchanged items which should be deleted)
let total_diffs = summary["total_diffs"].as_u64().unwrap();
assert!(total_diffs > 0, "Should have at least some diffs");
// Verify ahead/behind counts
let total_ahead = summary["total_ahead"].as_u64().unwrap();
let total_behind = summary["total_behind"].as_u64().unwrap();
assert!(total_ahead > 0, "Should have items ahead");
assert!(total_behind > 0, "Should have items behind");
// Verify conflicts (items that are both ahead and behind)
let conflicts = summary["conflicts"].as_u64().unwrap();
assert!(conflicts >= 1, "Should have at least 1 conflict (flow)");
// Verify per-item-type counts
assert!(summary["scripts_changed"].as_u64().unwrap() > 0, "Should have script changes");
assert!(summary["flows_changed"].as_u64().unwrap() > 0, "Should have flow changes");
assert!(summary["apps_changed"].as_u64().unwrap() > 0, "Should have app changes");
assert!(summary["resources_changed"].as_u64().unwrap() > 0, "Should have resource changes");
assert!(summary["variables_changed"].as_u64().unwrap() > 0, "Should have variable changes");
assert!(summary["resource_types_changed"].as_u64().unwrap() > 0, "Should have resource_type changes");
// Note: folders_changed may be 0 if folder comparison didn't detect changes
// assert!(summary["folders_changed"].as_u64().unwrap() > 0, "Should have folder changes");
// ==============================================================
// Individual Diff Assertions
// ==============================================================
// Scenario 1: New in parent
let new_in_parent = diffs.iter()
.find(|d| d["path"] == "f/shared/new_in_parent" && d["kind"] == "script")
.expect("Should find new_in_parent diff");
assert_eq!(new_in_parent["ahead"].as_i64().unwrap(), 1, "new_in_parent should be ahead");
assert_eq!(new_in_parent["behind"].as_i64().unwrap(), 0, "new_in_parent should not be behind");
assert_eq!(new_in_parent["has_changes"].as_bool().unwrap(), true, "new_in_parent should have changes");
assert_eq!(new_in_parent["exists_in_source"].as_bool().unwrap(), true, "new_in_parent should exist in source");
assert_eq!(new_in_parent["exists_in_fork"].as_bool().unwrap(), false, "new_in_parent should not exist in fork");
// Scenario 2: New in fork
let new_in_fork = diffs.iter()
.find(|d| d["path"] == "f/shared/new_in_fork" && d["kind"] == "script")
.expect("Should find new_in_fork diff");
assert_eq!(new_in_fork["ahead"].as_i64().unwrap(), 0, "new_in_fork should not be ahead");
assert_eq!(new_in_fork["behind"].as_i64().unwrap(), 1, "new_in_fork should be behind");
assert_eq!(new_in_fork["has_changes"].as_bool().unwrap(), true, "new_in_fork should have changes");
assert_eq!(new_in_fork["exists_in_source"].as_bool().unwrap(), false, "new_in_fork should not exist in source");
assert_eq!(new_in_fork["exists_in_fork"].as_bool().unwrap(), true, "new_in_fork should exist in fork");
// Scenario 5: Conflict
let conflict_flow = diffs.iter()
.find(|d| d["path"] == "f/shared/original_flow" && d["kind"] == "flow")
.expect("Should find conflict flow diff");
assert!(conflict_flow["ahead"].as_i64().unwrap() > 0, "conflict should be ahead");
assert!(conflict_flow["behind"].as_i64().unwrap() > 0, "conflict should be behind");
assert_eq!(conflict_flow["has_changes"].as_bool().unwrap(), true, "conflict should have changes");
assert_eq!(conflict_flow["exists_in_source"].as_bool().unwrap(), true, "conflict should exist in source");
assert_eq!(conflict_flow["exists_in_fork"].as_bool().unwrap(), true, "conflict should exist in fork");
// Scenario 6: Deleted in fork
let deleted = diffs.iter()
.find(|d| d["path"] == "f/shared/to_delete" && d["kind"] == "script")
.expect("Should find deleted diff");
assert_eq!(deleted["exists_in_source"].as_bool().unwrap(), true, "deleted should exist in source");
assert_eq!(deleted["exists_in_fork"].as_bool().unwrap(), false, "deleted should not exist in fork (archived)");
assert_eq!(deleted["has_changes"].as_bool().unwrap(), true, "deleted should have changes");
// Scenario 7: Rename (should show as two entries)
let old_name = diffs.iter()
.find(|d| d["path"] == "f/shared/old_name" && d["kind"] == "resource");
let new_name = diffs.iter()
.find(|d| d["path"] == "f/shared/new_name" && d["kind"] == "resource");
// At least one of these should exist (depending on how the comparison handles renames)
assert!(old_name.is_some() || new_name.is_some(), "Should find at least one rename-related diff");
// ==============================================================
// Database State Assertions
// ==============================================================
// Verify has_changes was cached for items that have changes
let cached_new_in_parent = sqlx::query!(
"SELECT has_changes, exists_in_source, exists_in_fork FROM workspace_diff
WHERE path = 'f/shared/new_in_parent' AND kind = 'script' AND source_workspace_id = 'test-workspace'"
)
.fetch_one(&db)
.await?;
assert_eq!(cached_new_in_parent.has_changes, Some(true), "has_changes should be cached as true");
assert_eq!(cached_new_in_parent.exists_in_source, Some(true), "exists_in_source should be cached");
assert_eq!(cached_new_in_parent.exists_in_fork, Some(false), "exists_in_fork should be cached");
// Verify unchanged items were deleted from workspace_diff
let unchanged_original_script = sqlx::query!(
"SELECT has_changes FROM workspace_diff
WHERE path = 'f/shared/original_script' AND kind = 'script' AND source_workspace_id = 'test-workspace'"
)
.fetch_optional(&db)
.await?;
// The unchanged item should either be deleted or marked as has_changes = false
// Based on the code, items with has_changes = false are deleted
if let Some(record) = unchanged_original_script {
assert_ne!(record.has_changes, Some(false), "unchanged items with has_changes=false should be deleted");
}
// ==============================================================
// Lazy Evaluation Test
// ==============================================================
// Create a new diff entry with NULL has_changes
sqlx::query!(
"INSERT INTO workspace_diff
(source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)
VALUES ('test-workspace', 'wm-fork-test-workspace', 'f/shared/lazy_test', 'script', 1, 0, NULL)
ON CONFLICT DO NOTHING"
)
.execute(&db)
.await?;
// Call the endpoint again
let _comparison2: serde_json::Value = client
.client()
.get(&format!("{base_url}/w/test-workspace/workspaces/compare/wm-fork-test-workspace"))
.send()
.await?
.json()
.await?;
// Verify the lazy_test entry was evaluated (should be deleted since it doesn't exist)
let lazy_test = sqlx::query!(
"SELECT has_changes FROM workspace_diff
WHERE path = 'f/shared/lazy_test' AND kind = 'script' AND source_workspace_id = 'test-workspace'"
)
.fetch_optional(&db)
.await?;
// Should be deleted since the item doesn't actually exist in either workspace
assert!(lazy_test.is_none(), "Non-existent item should be deleted from workspace_diff");
Ok(())
}

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.623.0
version: 1.621.2
title: Windmill API
contact:
@@ -18121,6 +18121,7 @@ components:
- visible_to_owner
- tag
ExportableCompletedJob:
type: object
description: Completed job with full data for export/import operations
@@ -22275,8 +22276,7 @@ components:
properties:
kind:
type: string
enum:
["script", "flow", "app", "resource", "variable", "resource_type"]
enum: ["script", "flow", "app", "resource", "variable"]
description: Type of the item
path:
type: string
@@ -22308,8 +22308,6 @@ components:
- apps_changed
- resources_changed
- variables_changed
- resource_types_changed
- folders_changed
- conflicts
properties:
total_diffs:
@@ -22336,12 +22334,6 @@ components:
variables_changed:
type: integer
description: Number of variables with differences
resource_types_changed:
type: integer
description: Number of resource types with differences
folders_changed:
type: integer
description: Number of folders with differences
conflicts:
type: integer
description: Number of items that are both ahead and behind (conflicts)
@@ -22588,13 +22580,14 @@ components:
type: object
properties:
service_name:
$ref: "#/components/schemas/NativeServiceName"
$ref: '#/components/schemas/NativeServiceName'
oauth_data:
nullable: true
$ref: "#/components/schemas/WorkspaceOAuthConfig"
$ref: '#/components/schemas/WorkspaceOAuthConfig'
required:
- service_name
WorkspaceOAuthConfig:
type: object
properties:
@@ -22625,7 +22618,7 @@ components:
type: string
enum: [webhook]
request_type:
$ref: "#/components/schemas/WebhookRequestType"
$ref: '#/components/schemas/WebhookRequestType'
required:
- type
- request_type
@@ -22641,10 +22634,12 @@ components:
type: object
properties:
redirect_uri:
type: string
type:
string
required:
- redirect_uri
NativeTriggerData:
type: object
description: Data for creating or updating a native trigger

View File

@@ -1391,7 +1391,6 @@ async fn delete_app(
deployed_object,
Some(format!("App '{}' deleted", path)),
true,
None,
)
.await?;

View File

@@ -30,11 +30,7 @@ async fn current_database(conn: &mut PgConnection) -> Result<String, MigrateErro
}
lazy_static::lazy_static! {
pub static ref OVERRIDDEN_MIGRATIONS: std::collections::HashMap<i64, String> = vec![(20220123221903, include_str!(
"../../migrations/20220123221903_first.up.sql"
).replace("create SCHEMA IF NOT exists extensions;", "")
.replace("create extension if not exists \"uuid-ossp\" with schema extensions;", "")),
(20221207103910, include_str!(
pub static ref OVERRIDDEN_MIGRATIONS: std::collections::HashMap<i64, String> = vec![(20221207103910, include_str!(
"../../custom_migrations/create_workspace_without_md5.sql"
).to_string()),
(20240216100535, include_str!(

View File

@@ -1476,7 +1476,6 @@ async fn archive_flow_by_path(
}
)),
true,
None,
)
.await?;
@@ -1592,7 +1591,6 @@ async fn delete_flow_by_path(
},
Some(format!("Flow '{}' deleted", path)),
true,
None,
)
.await?;

View File

@@ -287,7 +287,6 @@ async fn create_folder(
DeployedObject::Folder { path: format!("f/{}", ng.name) },
Some(format!("Folder '{}' created", ng.name)),
true,
None,
)
.await?;
@@ -468,7 +467,6 @@ async fn update_folder(
DeployedObject::Folder { path: format!("f/{}", name) },
Some(format!("Folder '{}' updated", name)),
true,
None,
)
.await?;
@@ -664,7 +662,6 @@ async fn delete_folder(
DeployedObject::Folder { path: format!("f/{}", name) },
Some(format!("Folder '{}' deleted", name)),
true,
None,
)
.await?;

View File

@@ -169,7 +169,6 @@ async fn add_granular_acl(
DeployedObject::Folder { path: format!("f/{}", path) },
Some(format!("Folder '{}' changed permissions", path)),
true,
None,
)
.await?
}
@@ -312,7 +311,6 @@ async fn remove_granular_acl(
DeployedObject::Folder { path: format!("f/{}", path) },
Some(format!("Folder '{}' changed permissions", path)),
true,
None,
)
.await?
}

View File

@@ -291,7 +291,6 @@ async fn create_group(
windmill_git_sync::DeployedObject::Group { name: ng.name.clone() },
Some(format!("Created group '{}'", &ng.name)),
true,
None,
)
.await?;
@@ -515,7 +514,6 @@ async fn delete_group(
windmill_git_sync::DeployedObject::Group { name: name.clone() },
Some(format!("Deleted group '{}'", &name)),
true,
None,
)
.await?;
@@ -575,7 +573,6 @@ async fn update_group(
windmill_git_sync::DeployedObject::Group { name: name.clone() },
Some(format!("Updated group '{}'", &name)),
true,
None,
)
.await?;
@@ -640,7 +637,6 @@ async fn add_user(
windmill_git_sync::DeployedObject::Group { name: name.clone() },
Some(format!("Added user to group '{}'", &name)),
true,
None,
)
.await?;
@@ -918,7 +914,6 @@ async fn remove_user(
windmill_git_sync::DeployedObject::Group { name: name.clone() },
Some(format!("Removed user from group '{}'", &name)),
true,
None,
)
.await?;

View File

@@ -781,7 +781,6 @@ async fn create_resource(
DeployedObject::Resource { path: resource.path.clone(), parent_path: None },
Some(format!("Resource '{}' created", resource.path.clone())),
true,
None,
)
.await?;
@@ -843,7 +842,6 @@ async fn delete_resource(
DeployedObject::Resource { path: path.to_string(), parent_path: Some(path.to_string()) },
Some(format!("Resource '{}' deleted", path)),
true,
None,
)
.await?;
@@ -902,7 +900,6 @@ async fn delete_resources_bulk(
},
Some(format!("Resource '{}' deleted", path)),
true,
None,
)
}))
.await?;
@@ -1013,9 +1010,6 @@ async fn update_resource(
.await?;
tx.commit().await?;
// Detect if this was a rename operation
let old_path_if_renamed = if npath != path { Some(path) } else { None };
handle_deployment_metadata(
&authed.email,
&authed.username,
@@ -1024,7 +1018,6 @@ async fn update_resource(
DeployedObject::Resource { path: npath.to_string(), parent_path: Some(path.to_string()) },
Some(format!("Resource '{}' updated", npath)),
true,
old_path_if_renamed,
)
.await?;
@@ -1085,7 +1078,6 @@ async fn update_resource_value(
DeployedObject::Resource { path: path.to_string(), parent_path: Some(path.to_string()) },
None,
true,
None,
)
.await?;
@@ -1247,7 +1239,6 @@ async fn create_resource_type(
resource_type.name.clone()
)),
true,
None,
)
.await?;
@@ -1325,7 +1316,6 @@ async fn delete_resource_type(
DeployedObject::ResourceType { path: name.clone() },
None,
true,
None,
)
.await?;
@@ -1381,7 +1371,6 @@ async fn update_resource_type(
DeployedObject::ResourceType { path: name.clone() },
None,
true,
None,
)
.await?;

View File

@@ -326,7 +326,6 @@ async fn create_schedule(
DeployedObject::Schedule { path: ns.path.clone() },
Some(format!("Schedule '{}' created", ns.path.clone())),
true,
None,
)
.await?;
@@ -477,7 +476,6 @@ async fn edit_schedule(
DeployedObject::Schedule { path: path.to_string() },
None,
true,
None,
)
.await?;
@@ -743,7 +741,6 @@ pub async fn set_enabled(
DeployedObject::Schedule { path: path.to_string() },
None,
true,
None,
)
.await?;
@@ -864,7 +861,6 @@ async fn delete_schedule(
DeployedObject::Schedule { path: path.to_string() },
Some(format!("Schedule '{}' deleted", path)),
true,
None,
)
.await?;
@@ -1001,8 +997,7 @@ async fn set_default_error_handler(
DeployedObject::Schedule { path: updated_schedule_path },
None,
true,
None,
)
)
.await?;
}
}

View File

@@ -563,7 +563,6 @@ struct HandleDeploymentMetadata {
w_id: String,
obj: DeployedObject,
deployment_message: Option<String>,
renamed_from: Option<String>,
}
impl HandleDeploymentMetadata {
@@ -576,7 +575,6 @@ impl HandleDeploymentMetadata {
self.obj,
self.deployment_message,
false,
self.renamed_from.as_deref(),
)
.await
}
@@ -1196,10 +1194,9 @@ async fn create_script_internal<'c>(
obj: DeployedObject::Script {
hash: hash.clone(),
path: script_path.clone(),
parent_path: p_path_opt.clone(),
parent_path: p_path_opt,
},
deployment_message: ns.deployment_message,
renamed_from: p_path_opt,
}),
))
}
@@ -1967,7 +1964,6 @@ async fn archive_script_by_path(
},
Some(format!("Script '{}' archived", path)),
true,
None,
)
.await?;
@@ -2189,7 +2185,6 @@ async fn delete_script_by_path(
},
Some(format!("Script '{}' deleted", path)),
true,
None,
)
.await?;
@@ -2294,7 +2289,6 @@ async fn delete_scripts_bulk(
DeployedObject::Script { hash: ScriptHash(0), path: path.clone(), parent_path: None },
Some(format!("Script '{}' deleted", path)),
true,
None,
)
}))
.await?;

View File

@@ -436,7 +436,6 @@ async fn create_trigger<T: TriggerCrud>(
T::get_deployed_object(new_path.clone()),
Some(format!("{} '{}' created", T::DEPLOYMENT_NAME, new_path)),
true,
None,
)
.await?;
@@ -530,7 +529,6 @@ async fn update_trigger<T: TriggerCrud>(
T::get_deployed_object(new_path.clone()),
Some(format!("{} '{}' updated", T::DEPLOYMENT_NAME, new_path)),
true,
None,
)
.await?;
@@ -631,7 +629,6 @@ async fn set_trigger_mode<T: TriggerCrud>(
T::get_deployed_object(path.to_owned()),
Some(format!("{} trigger '{}' updated", T::DEPLOYMENT_NAME, path)),
true,
None,
)
.await?;

View File

@@ -314,7 +314,6 @@ pub async fn create_many_http_triggers(
windmill_git_sync::DeployedObject::HttpTrigger { path: http_trigger.base.path.clone() },
Some(format!("HTTP trigger '{}' created", http_trigger.base.path)),
true,
None,
)
.await
.map_err(|err| error_wrapper(&http_trigger.config.route_path, err.into()))?;

View File

@@ -1239,7 +1239,6 @@ async fn accept_invite(
windmill_git_sync::DeployedObject::User { email: authed.email.clone() },
Some(format!("User '{}' accepted invite", &authed.email)),
true,
None,
)
.await?;
webhook.send_instance_event(InstanceEvent::UserJoinedWorkspace {
@@ -1444,7 +1443,6 @@ async fn update_workspace_user(
windmill_git_sync::DeployedObject::User { email: user_email.clone() },
Some(format!("Updated user '{}'", &user_email)),
true,
None,
)
.await?;
@@ -1587,7 +1585,6 @@ async fn convert_user_to_group(
&user_info.email, primary_group_name, role
)),
true,
None,
)
.await?;
@@ -1899,7 +1896,6 @@ async fn delete_workspace_user(
&email_to_delete
)),
true,
None,
)
.await?;

View File

@@ -399,7 +399,6 @@ async fn create_variable(
DeployedObject::Variable { path: variable.path.clone(), parent_path: None },
Some(format!("Variable '{}' created", variable.path.clone())),
true,
None,
)
.await?;
@@ -488,7 +487,6 @@ async fn delete_variable(
DeployedObject::Variable { path: path.to_string(), parent_path: Some(path.to_string()) },
Some(format!("Variable '{}' deleted", path)),
true,
None,
)
.await?;
@@ -570,7 +568,6 @@ async fn delete_variables_bulk(
},
Some(format!("Variable '{}' deleted", path)),
true,
None,
)
}))
.await?;
@@ -796,9 +793,6 @@ async fn update_variable(
tx.commit().await?;
// Detect if this was a rename operation
let old_path_if_renamed = if npath != path { Some(path) } else { None };
handle_deployment_metadata(
&authed.email,
&authed.username,
@@ -807,7 +801,6 @@ async fn update_variable(
DeployedObject::Variable { path: npath.clone(), parent_path: Some(path.to_string()) },
None,
true,
old_path_if_renamed,
)
.await?;

View File

@@ -948,7 +948,6 @@ async fn edit_deploy_to(
DeployedObject::Settings { setting_type: "deploy_to".to_string() },
None,
false,
None,
)
.await?;
@@ -1049,7 +1048,6 @@ async fn edit_webhook(
DeployedObject::Settings { setting_type: "webhook".to_string() },
None,
false,
None,
)
.await?;
@@ -1116,7 +1114,6 @@ async fn edit_copilot_config(
windmill_git_sync::DeployedObject::Settings { setting_type: "ai_config".to_string() },
Some("AI configuration updated".to_string()),
false,
None,
)
.await?;
@@ -1210,7 +1207,6 @@ async fn edit_large_file_storage_config(
},
Some("Large file storage configuration updated".to_string()),
false,
None,
)
.await?;
@@ -1732,7 +1728,6 @@ async fn edit_git_sync_config(
windmill_git_sync::DeployedObject::Settings { setting_type: "git_sync".to_string() },
Some("Git sync configuration updated".to_string()),
false,
None,
)
.await?;
@@ -1856,7 +1851,6 @@ async fn edit_git_sync_repository(
if repo_exists { "updated" } else { "added" }
)),
false,
None,
)
.await?;
@@ -1972,7 +1966,6 @@ async fn delete_git_sync_repository(
request.git_repo_resource_path
)),
false,
None,
)
.await?;
@@ -2115,7 +2108,6 @@ async fn edit_default_scripts(
windmill_git_sync::DeployedObject::Settings { setting_type: "default_scripts".to_string() },
Some("Default scripts configuration updated".to_string()),
false,
None,
)
.await?;
@@ -2198,7 +2190,6 @@ async fn edit_default_app(
windmill_git_sync::DeployedObject::Settings { setting_type: "default_app".to_string() },
Some("Default app configuration updated".to_string()),
false,
None,
)
.await?;
@@ -2331,7 +2322,6 @@ async fn edit_error_handler(
windmill_git_sync::DeployedObject::Settings { setting_type: "error_handler".to_string() },
Some("Error handler configuration updated".to_string()),
false,
None,
)
.await?;
@@ -2407,7 +2397,6 @@ async fn edit_success_handler(
windmill_git_sync::DeployedObject::Settings { setting_type: "success_handler".to_string() },
Some("Success handler configuration updated".to_string()),
false,
None,
)
.await?;
@@ -2586,7 +2575,6 @@ async fn set_encryption_key(
windmill_git_sync::DeployedObject::Key { key_type: "encryption_key".to_string() },
Some("Encryption key updated".to_string()),
false,
None,
)
.await?;
@@ -3966,7 +3954,6 @@ async fn add_user(
windmill_git_sync::DeployedObject::User { email: nu.email.clone() },
Some(format!("Added user '{}' to workspace", &nu.email)),
true,
None,
)
.await?;
@@ -4212,7 +4199,6 @@ async fn change_workspace_name(
windmill_git_sync::DeployedObject::Settings { setting_type: "workspace_name".to_string() },
Some(format!("Workspace name updated to {}", &rw.new_name)),
false,
None,
)
.await?;
@@ -4247,7 +4233,6 @@ async fn change_workspace_color(
DeployedObject::Settings { setting_type: "workspace_color".to_string() },
None,
false,
None,
)
.await?;
@@ -4361,7 +4346,6 @@ async fn mute_critical_alerts(
DeployedObject::Settings { setting_type: "critical_alerts".to_string() },
None,
false,
None,
)
.await?;
@@ -4433,7 +4417,6 @@ async fn update_operator_settings(
},
Some("Operator settings updated".to_string()),
false,
None,
)
.await?;
@@ -4459,8 +4442,6 @@ pub struct CompareSummary {
pub apps_changed: usize,
pub resources_changed: usize,
pub variables_changed: usize,
pub resource_types_changed: usize,
pub folders_changed: usize,
pub conflicts: usize, // Items that are both ahead and behind
}
@@ -4571,19 +4552,6 @@ async fn compare_workspaces(
compare_two_variables(&db, &source_workspace_id, &fork_workspace_id, &item.path)
.await?,
),
"resource_type" => Some(
compare_two_resource_types(
&db,
&source_workspace_id,
&fork_workspace_id,
&item.path,
)
.await?,
),
"folder" => Some(
compare_two_folders(&db, &source_workspace_id, &fork_workspace_id, &item.path)
.await?,
),
k => {
tracing::error!("Received unrecognized item kind `{k}` with path: `{}` while computing diff of {fork_workspace_id} and {source_workspace_id} workspaces. Skipping this item", item.path);
None
@@ -4664,11 +4632,6 @@ async fn compare_workspaces(
.iter()
.filter(|s| s.kind == "variable")
.count(),
resource_types_changed: visible_diffs
.iter()
.filter(|s| s.kind == "resource_type")
.count(),
folders_changed: visible_diffs.iter().filter(|s| s.kind == "folder").count(),
conflicts: visible_diffs
.iter()
.filter(|s| s.ahead > 0 && s.behind > 0)
@@ -4794,33 +4757,6 @@ async fn query_visible_items<'c>(
.fetch_all(&mut **tx)
.await?
}
"folder" => {
let a: Vec<String> = paths_vec
.iter()
.map(|p| p.strip_prefix("f/").unwrap_or(p.as_str()).to_string())
.collect();
sqlx::query_scalar!(
"SELECT name FROM folder
WHERE workspace_id = $1 AND name = ANY($2)",
workspace_id,
&a,
)
.fetch_all(&mut **tx)
.await?
.into_iter()
.map(|p| format!("f/{p}"))
.collect()
}
"resource_type" => {
sqlx::query_scalar!(
"SELECT name FROM resource_type
WHERE workspace_id = $1 AND name = ANY($2)",
workspace_id,
&paths_vec
)
.fetch_all(&mut **tx)
.await?
}
_ => vec![], // Unknown kind
};
@@ -5094,102 +5030,3 @@ async fn compare_two_variables(
exists_in_fork: target_variable.is_some(),
});
}
async fn compare_two_resource_types(
db: &DB,
source_workspace_id: &str,
fork_workspace_id: &str,
name: &str,
) -> Result<ItemComparison> {
// Get resource type from each workspace
let source_resource_type = sqlx::query!(
"SELECT schema, description, format_extension
FROM resource_type
WHERE workspace_id = $1 AND name = $2",
source_workspace_id,
name
)
.fetch_optional(db)
.await?;
let target_resource_type = sqlx::query!(
"SELECT schema, description, format_extension
FROM resource_type
WHERE workspace_id = $1 AND name = $2",
fork_workspace_id,
name
)
.fetch_optional(db)
.await?;
let mut has_changes = false;
// Check metadata differences
if let (Some(source), Some(target)) = (&source_resource_type, &target_resource_type) {
if source.schema != target.schema
|| source.description != target.description
|| source.format_extension != target.format_extension
{
has_changes = true;
}
} else if source_resource_type.is_some() || target_resource_type.is_some() {
// The resource type exists in one of source or target, but not the other, this is considered as a change
has_changes = true
}
return Ok(ItemComparison {
has_changes,
exists_in_source: source_resource_type.is_some(),
exists_in_fork: target_resource_type.is_some(),
});
}
async fn compare_two_folders(
db: &DB,
source_workspace_id: &str,
fork_workspace_id: &str,
name: &str,
) -> Result<ItemComparison> {
// Get folder from each workspace
let source_folder = sqlx::query!(
"SELECT display_name, owners, extra_perms, summary
FROM folder
WHERE workspace_id = $1 AND name = $2",
source_workspace_id,
name.strip_prefix("f/"),
)
.fetch_optional(db)
.await?;
let target_folder = sqlx::query!(
"SELECT display_name, owners, extra_perms, summary
FROM folder
WHERE workspace_id = $1 AND name = $2",
fork_workspace_id,
name.strip_prefix("f/"),
)
.fetch_optional(db)
.await?;
let mut has_changes = false;
// Check metadata differences
if let (Some(source), Some(target)) = (&source_folder, &target_folder) {
if source.display_name != target.display_name
|| source.owners != target.owners
|| source.extra_perms != target.extra_perms
|| source.summary != target.summary
{
has_changes = true;
}
} else if source_folder.is_some() || target_folder.is_some() {
// The folder exists in one of source or target, but not the other, this is considered as a change
has_changes = true
}
return Ok(ItemComparison {
has_changes,
exists_in_source: source_folder.is_some(),
exists_in_fork: target_folder.is_some(),
});
}

View File

@@ -13,55 +13,14 @@ use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
use crate::{jwt::decode_without_verify, utils::configure_client, worker::HttpClient};
/// Configuration required for agent mode. Both fields are mandatory when running in agent mode.
#[derive(Clone)]
pub struct AgentConfig {
pub agent_token: String,
pub base_internal_url: String,
}
impl AgentConfig {
pub fn from_env() -> Result<Self, AgentConfigError> {
let agent_token = std::env::var("AGENT_TOKEN")
.map_err(|_| AgentConfigError::MissingAgentToken)?;
let base_internal_url = std::env::var("BASE_INTERNAL_URL")
.map_err(|_| AgentConfigError::MissingBaseInternalUrl)?;
Ok(Self { agent_token, base_internal_url })
}
pub fn build_http_client(&self, worker_suffix: &str) -> HttpClient {
build_agent_http_client(worker_suffix, &self.agent_token, &self.base_internal_url)
}
}
#[derive(Debug)]
pub enum AgentConfigError {
MissingAgentToken,
MissingBaseInternalUrl,
}
impl std::fmt::Display for AgentConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AgentConfigError::MissingAgentToken => {
write!(f, "AGENT_TOKEN environment variable is not set but required for agent mode")
}
AgentConfigError::MissingBaseInternalUrl => {
write!(f, "BASE_INTERNAL_URL environment variable is not set but required for agent mode")
}
}
}
}
impl std::error::Error for AgentConfigError {}
lazy_static! {
pub static ref AGENT_TOKEN: String = std::env::var("AGENT_TOKEN").unwrap_or_default();
pub static ref DECODED_AGENT_TOKEN: Option<AgentAuth> = {
let agent_token = std::env::var("AGENT_TOKEN");
if let Ok(token) = agent_token {
decode_without_verify::<AgentAuth>(token.trim_start_matches(AGENT_JWT_PREFIX)).ok()
} else {
if AGENT_TOKEN.is_empty() {
None
} else {
decode_without_verify::<AgentAuth>(AGENT_TOKEN.trim_start_matches(AGENT_JWT_PREFIX))
.ok()
}
};
}
@@ -69,7 +28,7 @@ lazy_static! {
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AgentAuth {
pub worker_group: String,
pub suffix: Option<String>,
pub suffix: Option<String>,
pub tags: Vec<String>,
pub exp: Option<usize>,
}
@@ -78,8 +37,8 @@ pub const AGENT_JWT_PREFIX: &str = "jwt_agent_";
pub fn build_agent_http_client(
worker_suffix: &str,
agent_token: &str,
base_internal_url: &str,
agent_token: Option<String>,
base_internal_url: Option<String>,
) -> HttpClient {
let client = ClientBuilder::new(
configure_client(
@@ -99,7 +58,9 @@ pub fn build_agent_http_client(
"{}{}_{}",
AGENT_JWT_PREFIX,
worker_suffix,
agent_token.trim_start_matches(AGENT_JWT_PREFIX)
agent_token
.unwrap_or(AGENT_TOKEN.clone())
.trim_start_matches(AGENT_JWT_PREFIX)
);
headers.insert(
"Authorization",
@@ -115,7 +76,7 @@ pub fn build_agent_http_client(
))
.build();
HttpClient { client, base_internal_url: base_internal_url.to_string() }
HttpClient { client, base_internal_url }
}
#[derive(Deserialize, Serialize)]

View File

@@ -151,6 +151,10 @@ lazy_static::lazy_static! {
pub static ref HUB_API_SECRET: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
}
lazy_static::lazy_static! {
pub static ref AGENT_TOKEN: String = std::env::var("AGENT_TOKEN").unwrap_or_default();
}
#[derive(Clone)]
pub struct ModeAndAddons {
pub indexer: bool,
@@ -1186,8 +1190,7 @@ mod tests {
#[test]
fn test_merge_nested_raw_values_to_array_complex_types() {
let val1 =
serde_json::value::RawValue::from_string("{\"name\":\"Alice\"}".to_string()).unwrap();
let val1 = serde_json::value::RawValue::from_string("{\"name\":\"Alice\"}".to_string()).unwrap();
let val2 = serde_json::value::RawValue::from_string("[1,2,3]".to_string()).unwrap();
let val3 = serde_json::value::RawValue::from_string("\"text\"".to_string()).unwrap();
let val4 = serde_json::value::RawValue::from_string("null".to_string()).unwrap();
@@ -1233,13 +1236,7 @@ mod tests {
let inner3 = vec![val3];
let inner4 = vec![val4];
let inner5 = vec![val5];
let nested = vec![
inner1.iter(),
inner2.iter(),
inner3.iter(),
inner4.iter(),
inner5.iter(),
];
let nested = vec![inner1.iter(), inner2.iter(), inner3.iter(), inner4.iter(), inner5.iter()];
let result = merge_nested_raw_values_to_array(nested.into_iter());

View File

@@ -274,10 +274,13 @@ lazy_static::lazy_static! {
pub const ROOT_CACHE_NOMOUNT_DIR: &str = concatcp!(TMP_DIR, "/cache_nomount/");
pub static MIN_VERSION_IS_LATEST: AtomicBool = AtomicBool::new(false);
const DEFAULT_BASE_INTERNAL_URL: &str = "http://localhost:8000";
#[derive(Clone)]
pub struct HttpClient {
pub client: ClientWithMiddleware,
pub base_internal_url: String,
pub base_internal_url: Option<String>,
}
impl Deref for HttpClient {
@@ -295,7 +298,10 @@ impl HttpClient {
headers: Option<HeaderMap>,
body: &T,
) -> anyhow::Result<R> {
let base_url = self.base_internal_url.clone();
let base_url = self
.base_internal_url
.clone()
.unwrap_or(DEFAULT_BASE_INTERNAL_URL.to_owned());
let response_builder = self.client.post(format!("{}{}", base_url, url)).json(body);
@@ -321,7 +327,11 @@ impl HttpClient {
}
pub async fn get<R: DeserializeOwned>(&self, url: &str) -> anyhow::Result<R> {
let base_url = self.base_internal_url.clone();
let base_url = self
.base_internal_url
.clone()
.unwrap_or(DEFAULT_BASE_INTERNAL_URL.to_owned());
let response = self
.client
.get(format!("{}{}", base_url, url))

View File

@@ -17,7 +17,6 @@ pub async fn handle_deployment_metadata<'c>(
_obj: DeployedObject,
_deployment_message: Option<String>,
_skip_db_insert: bool,
_renamed_from: Option<&str>,
) -> Result<()> {
// Git sync is an enterprise feature and not part of the open-source version
return Ok(());

View File

@@ -39,63 +39,3 @@ pub fn extract_resource_types_from_schema(schema: &SchemaType) -> HashSet<String
}
resource_types
}
/// Transform a JSON schema for maximum MCP client compatibility.
///
/// Some MCP clients (e.g., n8n) have limited JSON Schema support:
/// - `integer` type is not supported (convert to `number`)
pub fn make_schema_compatible(schema: &mut Value) {
let Value::Object(obj) = schema else { return };
// 1. Convert integer to number
if let Some(type_val) = obj.get_mut("type") {
match type_val {
Value::String(s) if s == "integer" => *s = "number".to_string(),
Value::Array(arr) => {
for item in arr.iter_mut() {
if let Value::String(s) = item {
if s == "integer" {
*s = "number".to_string();
}
}
}
}
_ => {}
}
}
// Recursively process nested schemas
if let Some(Value::Object(props)) = obj.get_mut("properties") {
for value in props.values_mut() {
make_schema_compatible(value);
}
}
if let Some(items) = obj.get_mut("items") {
make_schema_compatible(items);
}
if let Some(additional) = obj.get_mut("additionalProperties") {
if additional.is_object() {
make_schema_compatible(additional);
}
}
if let Some(Value::Array(all_of)) = obj.get_mut("allOf") {
for s in all_of.iter_mut() {
make_schema_compatible(s);
}
}
if let Some(Value::Array(one_of)) = obj.get_mut("oneOf") {
for s in one_of.iter_mut() {
make_schema_compatible(s);
}
}
if let Some(Value::Array(any_of)) = obj.get_mut("anyOf") {
for s in any_of.iter_mut() {
make_schema_compatible(s);
}
}
}

View File

@@ -8,8 +8,6 @@ use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::sync::Arc;
use crate::common::schema::make_schema_compatible;
/// Represents an auto-generated endpoint tool from OpenAPI specification
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct EndpointTool {
@@ -39,12 +37,11 @@ pub fn endpoint_tool_to_mcp_tool(tool: &EndpointTool) -> Tool {
merge_schema_into(&mut combined_properties, &mut combined_required, schema);
}
let mut combined_schema = serde_json::json!({
let combined_schema = serde_json::json!({
"type": "object",
"properties": combined_properties,
"required": combined_required
});
make_schema_compatible(&mut combined_schema);
let description = format!("{}. {}", tool.description, tool.instructions);

View File

@@ -8,7 +8,7 @@ use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;
use crate::common::schema::{convert_schema_to_schema_type, make_schema_compatible};
use crate::common::schema::convert_schema_to_schema_type;
use crate::common::transform::transform_path;
use crate::common::types::{
FlowInfo, HubScriptInfo, ResourceInfo, ResourceType, SchemaType, ScriptInfo, ToolableItem,
@@ -147,18 +147,13 @@ pub fn create_tool_from_item<T: ToolableItem, B: McpBackend>(
backend.transform_schema_for_resources(&schema, resources_cache, resources_types);
let input_schema_map = match serde_json::to_value(schema_obj) {
Ok(mut value) => {
make_schema_compatible(&mut value);
match value {
serde_json::Value::Object(map) => map,
_ => {
tracing::warn!(
"Schema object for tool '{}' did not serialize to a JSON object, using empty schema.",
path
);
serde_json::Map::new()
}
}
Ok(serde_json::Value::Object(map)) => map,
Ok(_) => {
tracing::warn!(
"Schema object for tool '{}' did not serialize to a JSON object, using empty schema.",
path
);
serde_json::Map::new()
}
Err(e) => {
tracing::error!(

View File

@@ -468,19 +468,12 @@ impl QueryBuilder for AnthropicQueryBuilder {
events_str,
annotations,
used_websearch,
usage: anthropic_usage,
..
} = anthropic_sse_parser;
// Note: Tool call arguments events are already sent by the parser during streaming
// when content_block_stop is received
// Convert Anthropic usage to TokenUsage
let usage = anthropic_usage.map(|u| {
TokenUsage::from_input_output(u.input_tokens, u.output_tokens)
.with_cache(u.cache_read_input_tokens, u.cache_creation_input_tokens)
});
Ok(ParsedResponse::Text {
content: if accumulated_content.is_empty() {
None
@@ -491,7 +484,6 @@ impl QueryBuilder for AnthropicQueryBuilder {
events_str: Some(events_str),
annotations,
used_websearch,
usage,
})
}

View File

@@ -10,7 +10,6 @@ use crate::ai::{
image_handler::prepare_messages_for_api,
query_builder::{ParsedResponse, StreamEventProcessor},
types::StreamingEvent,
types::TokenUsage,
types::{OpenAIMessage, ToolDef},
};
use std::collections::HashMap;
@@ -143,7 +142,6 @@ impl BedrockQueryBuilder {
let mut events_str = String::new();
let mut accumulated_tool_calls: HashMap<String, StreamingToolCall> = HashMap::new();
let mut current_tool_use_id: Option<String> = None;
let mut usage: Option<TokenUsage> = None;
// Process stream events using shared parsing functions
loop {
@@ -183,30 +181,6 @@ impl BedrockQueryBuilder {
if bedrock_stream_event_is_block_stop(&event) {
current_tool_use_id = None;
}
// Extract usage from Metadata event
if let aws_sdk_bedrockruntime::types::ConverseStreamOutput::Metadata(
metadata,
) = &event
{
if let Some(token_usage) = metadata.usage() {
usage = Some(
TokenUsage::new(
Some(token_usage.input_tokens()),
Some(token_usage.output_tokens()),
Some(token_usage.total_tokens()),
)
.with_cache(
token_usage
.cache_read_input_tokens()
.map(|v| i32::try_from(v).unwrap_or(i32::MAX)),
token_usage
.cache_write_input_tokens()
.map(|v| i32::try_from(v).unwrap_or(i32::MAX)),
),
);
}
}
}
Ok(None) => break,
Err(e) => {
@@ -250,7 +224,6 @@ impl BedrockQueryBuilder {
},
annotations: Vec::new(),
used_websearch: false,
usage,
})
}
}

View File

@@ -617,7 +617,6 @@ impl QueryBuilder for GoogleAIQueryBuilder {
stream_event_processor,
annotations,
used_websearch,
usage: gemini_usage,
..
} = gemini_sse_parser;
@@ -631,10 +630,6 @@ impl QueryBuilder for GoogleAIQueryBuilder {
stream_event_processor.send(event, &mut events_str).await?;
}
// Convert Gemini usage metadata to TokenUsage
let usage = gemini_usage
.map(|u| TokenUsage::new(u.prompt_token_count, u.candidates_token_count, u.total_token_count));
Ok(ParsedResponse::Text {
content: if accumulated_content.is_empty() {
None
@@ -645,7 +640,6 @@ impl QueryBuilder for GoogleAIQueryBuilder {
events_str: Some(events_str),
annotations,
used_websearch,
usage,
})
}

View File

@@ -472,10 +472,6 @@ impl QueryBuilder for OpenAIQueryBuilder {
let mut parser = OpenAIResponsesSSEParser::new(stream_event_processor);
parser.parse_events(response).await?;
// Convert OpenAI Responses usage to TokenUsage
let usage =
parser.usage.map(|u| TokenUsage::new(u.input_tokens, u.output_tokens, u.total_tokens));
Ok(ParsedResponse::Text {
content: if parser.accumulated_content.is_empty() {
None
@@ -486,7 +482,6 @@ impl QueryBuilder for OpenAIQueryBuilder {
events_str: Some(parser.events_str),
annotations: parser.annotations,
used_websearch: parser.used_websearch,
usage,
})
}
@@ -518,9 +513,7 @@ impl QueryBuilder for OpenAIQueryBuilder {
"image_generation_call" => {
if output.status.as_deref() == Some("completed") {
if let Some(ref base64_image) = output.result {
return Ok(ParsedResponse::Image {
base64_data: base64_image.clone(),
});
return Ok(ParsedResponse::Image { base64_data: base64_image.clone() });
}
}
}

View File

@@ -117,9 +117,7 @@ impl QueryBuilder for OpenRouterQueryBuilder {
.and_then(|images| images.first())
{
if let Some(base64_data) = image.image_url.url.strip_prefix("data:image/png;base64,") {
return Ok(ParsedResponse::Image {
base64_data: base64_data.to_string(),
});
return Ok(ParsedResponse::Image { base64_data: base64_data.to_string() });
}
}

View File

@@ -11,7 +11,7 @@ use crate::ai::{
utils::should_use_structured_output_tool,
};
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug)]
#[serde(rename_all = "lowercase")]
pub enum ToolChoice {
#[allow(dead_code)]
@@ -19,12 +19,6 @@ pub enum ToolChoice {
Required,
}
/// Stream options for OpenAI API to include usage in streaming responses
#[derive(Serialize)]
pub struct StreamOptions {
pub include_usage: bool,
}
#[derive(Serialize)]
pub struct OpenAICompletionRequest<'a> {
pub model: &'a str,
@@ -40,14 +34,6 @@ pub struct OpenAICompletionRequest<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ToolChoice>,
pub stream: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_options: Option<StreamOptions>,
}
/// Result from building a request, includes both with and without stream_options
pub struct BuiltRequests {
pub with_usage: String,
pub without_usage: String,
}
/// Query builder for providers using the OpenAI-compatible completion endpoint
@@ -61,13 +47,12 @@ impl OtherQueryBuilder {
Self { provider_kind }
}
/// Build both request variants (with and without stream_options) to enable retry on incompatible providers
pub async fn build_text_requests(
async fn build_text_request(
&self,
args: &BuildRequestArgs<'_>,
client: &AuthedClient,
workspace_id: &str,
) -> Result<BuiltRequests, Error> {
) -> Result<String, Error> {
let prepared_messages =
prepare_messages_for_api(args.messages, client, workspace_id).await?;
@@ -108,21 +93,7 @@ impl OtherQueryBuilder {
None
};
// Build request with stream_options for usage tracking
let request_with_usage = OpenAICompletionRequest {
model: args.model,
messages: &prepared_messages,
tools: args.tools,
temperature: args.temperature,
max_completion_tokens: args.max_tokens,
response_format: response_format.clone(),
tool_choice: tool_choice.clone(),
stream: true,
stream_options: Some(StreamOptions { include_usage: true }),
};
// Build request without stream_options for providers that don't support it
let request_without_usage = OpenAICompletionRequest {
let request = OpenAICompletionRequest {
model: args.model,
messages: &prepared_messages,
tools: args.tools,
@@ -131,28 +102,10 @@ impl OtherQueryBuilder {
response_format,
tool_choice,
stream: true,
stream_options: None,
};
let with_usage = serde_json::to_string(&request_with_usage)
.map_err(|e| Error::internal_err(format!("Failed to serialize request: {}", e)))?;
let without_usage = serde_json::to_string(&request_without_usage)
.map_err(|e| Error::internal_err(format!("Failed to serialize request: {}", e)))?;
Ok(BuiltRequests { with_usage, without_usage })
}
async fn build_text_request(
&self,
args: &BuildRequestArgs<'_>,
client: &AuthedClient,
workspace_id: &str,
) -> Result<String, Error> {
// Default to returning the request with usage tracking
Ok(self
.build_text_requests(args, client, workspace_id)
.await?
.with_usage)
serde_json::to_string(&request)
.map_err(|e| Error::internal_err(format!("Failed to serialize request: {}", e)))
}
}
@@ -172,22 +125,6 @@ impl QueryBuilder for OtherQueryBuilder {
self.build_text_request(args, client, workspace_id).await
}
async fn build_request_without_usage(
&self,
args: &BuildRequestArgs<'_>,
client: &AuthedClient,
workspace_id: &str,
) -> Result<String, Error> {
Ok(self
.build_text_requests(args, client, workspace_id)
.await?
.without_usage)
}
fn supports_retry_without_usage(&self) -> bool {
true
}
async fn parse_image_response(
&self,
_response: reqwest::Response,
@@ -210,7 +147,6 @@ impl QueryBuilder for OtherQueryBuilder {
accumulated_tool_calls,
mut events_str,
stream_event_processor,
usage: openai_usage,
} = openai_sse_parser;
// Process streaming events with error handling
@@ -224,10 +160,6 @@ impl QueryBuilder for OtherQueryBuilder {
stream_event_processor.send(event, &mut events_str).await?;
}
// Convert OpenAI Chat Completions usage to TokenUsage
let usage =
openai_usage.map(|u| TokenUsage::new(u.prompt_tokens, u.completion_tokens, u.total_tokens));
Ok(ParsedResponse::Text {
content: if accumulated_content.is_empty() {
None
@@ -238,7 +170,6 @@ impl QueryBuilder for OtherQueryBuilder {
events_str: Some(events_str),
annotations: Vec::new(),
used_websearch: false,
usage,
})
}

View File

@@ -35,8 +35,6 @@ pub struct BuildRequestArgs<'a> {
pub has_websearch: bool,
}
use crate::ai::types::TokenUsage;
/// Response from AI provider
pub enum ParsedResponse {
Text {
@@ -45,7 +43,6 @@ pub enum ParsedResponse {
events_str: Option<String>,
annotations: Vec<UrlCitation>,
used_websearch: bool,
usage: Option<TokenUsage>,
},
Image {
base64_data: String,
@@ -66,23 +63,6 @@ pub trait QueryBuilder: Send + Sync {
workspace_id: &str,
) -> Result<String, Error>;
/// Build the request body without usage tracking (for retry on incompatible providers)
/// Default implementation just calls build_request (most providers don't need this)
async fn build_request_without_usage(
&self,
args: &BuildRequestArgs<'_>,
client: &AuthedClient,
workspace_id: &str,
) -> Result<String, Error> {
self.build_request(args, client, workspace_id).await
}
/// Whether this provider supports retry without usage tracking
/// Only OtherQueryBuilder (OpenAI-compatible providers) needs this
fn supports_retry_without_usage(&self) -> bool {
false
}
/// Parse the image response from the provider
async fn parse_image_response(
&self,

View File

@@ -38,22 +38,9 @@ pub struct OpenAIChoice {
pub delta: Option<OpenAIChoiceDelta>,
}
/// OpenAI Chat Completions API usage information (from final chunk with stream_options.include_usage)
#[derive(Deserialize, Debug, Clone, Default)]
pub struct OpenAIChatUsage {
#[serde(default)]
pub prompt_tokens: Option<i32>,
#[serde(default)]
pub completion_tokens: Option<i32>,
#[serde(default)]
pub total_tokens: Option<i32>,
}
#[derive(Deserialize)]
pub struct OpenAISSEEvent {
pub choices: Option<Vec<OpenAIChoice>>,
#[serde(default)]
pub usage: Option<OpenAIChatUsage>,
}
lazy_static::lazy_static! {
@@ -105,8 +92,6 @@ pub struct OpenAISSEParser {
pub accumulated_tool_calls: HashMap<i64, OpenAIToolCall>,
pub events_str: String,
pub stream_event_processor: StreamEventProcessor,
/// Token usage from final chunk (when stream_options.include_usage is true)
pub usage: Option<OpenAIChatUsage>,
}
impl OpenAISSEParser {
@@ -116,7 +101,6 @@ impl OpenAISSEParser {
accumulated_tool_calls: HashMap::new(),
events_str: String::new(),
stream_event_processor,
usage: None,
}
}
}
@@ -134,11 +118,6 @@ impl SSEParser for OpenAISSEParser {
.ok();
if let Some(event) = event {
// Extract usage from final chunk (when stream_options.include_usage is true)
if let Some(usage) = event.usage {
self.usage = Some(usage);
}
if let Some(mut choices) = event.choices.filter(|s| !s.is_empty()) {
if let Some(delta) = choices.remove(0).delta {
if let Some(content) = delta.content.filter(|s| !s.is_empty()) {
@@ -243,19 +222,6 @@ pub enum AnthropicDelta {
Unknown,
}
/// Anthropic usage information from message_delta event
#[derive(Deserialize, Debug, Clone)]
pub struct AnthropicUsage {
#[serde(default)]
pub input_tokens: Option<i32>,
#[serde(default)]
pub output_tokens: Option<i32>,
#[serde(default)]
pub cache_creation_input_tokens: Option<i32>,
#[serde(default)]
pub cache_read_input_tokens: Option<i32>,
}
/// Anthropic SSE event structure
#[derive(Deserialize, Debug)]
#[serde(tag = "type")]
@@ -269,10 +235,7 @@ pub enum AnthropicSSEEvent {
#[serde(rename = "content_block_stop")]
ContentBlockStop { index: usize },
#[serde(rename = "message_delta")]
MessageDelta {
#[serde(default)]
usage: Option<AnthropicUsage>,
},
MessageDelta {},
#[serde(rename = "message_stop")]
MessageStop {},
#[serde(rename = "ping")]
@@ -307,8 +270,6 @@ pub struct AnthropicSSEParser {
pub annotations: Vec<UrlCitation>,
/// Whether web search was used in this response
pub used_websearch: bool,
/// Token usage from message_delta event
pub usage: Option<AnthropicUsage>,
}
impl AnthropicSSEParser {
@@ -321,7 +282,6 @@ impl AnthropicSSEParser {
content_blocks: HashMap::new(),
annotations: Vec::new(),
used_websearch: false,
usage: None,
}
}
}
@@ -436,13 +396,9 @@ impl SSEParser for AnthropicSSEParser {
let error_msg = message.unwrap_or_else(|| "Unknown error".to_string());
tracing::error!("Anthropic streaming error: {}", error_msg);
}
AnthropicSSEEvent::MessageDelta { usage } => {
if let Some(u) = usage {
self.usage = Some(u);
}
}
// Ignore other events
AnthropicSSEEvent::MessageStart {}
| AnthropicSSEEvent::MessageDelta {}
| AnthropicSSEEvent::MessageStop {}
| AnthropicSSEEvent::Ping {}
| AnthropicSSEEvent::Unknown => {}
@@ -516,23 +472,10 @@ pub struct GeminiSSECandidate {
pub grounding_metadata: Option<GeminiGroundingMetadata>,
}
/// Gemini usage metadata from SSE response
#[derive(Deserialize, Debug, Clone)]
pub struct GeminiUsageMetadata {
#[serde(rename = "promptTokenCount", default)]
pub prompt_token_count: Option<i32>,
#[serde(rename = "candidatesTokenCount", default)]
pub candidates_token_count: Option<i32>,
#[serde(rename = "totalTokenCount", default)]
pub total_token_count: Option<i32>,
}
/// Gemini SSE event structure
#[derive(Deserialize, Debug)]
pub struct GeminiSSEEvent {
pub candidates: Option<Vec<GeminiSSECandidate>>,
#[serde(rename = "usageMetadata")]
pub usage_metadata: Option<GeminiUsageMetadata>,
}
/// Gemini SSE Parser for streaming responses
@@ -546,8 +489,6 @@ pub struct GeminiSSEParser {
pub annotations: Vec<UrlCitation>,
/// Whether web search was used in this response
pub used_websearch: bool,
/// Token usage from usageMetadata
pub usage: Option<GeminiUsageMetadata>,
}
impl GeminiSSEParser {
@@ -560,7 +501,6 @@ impl GeminiSSEParser {
tool_call_index: 0,
annotations: Vec::new(),
used_websearch: false,
usage: None,
}
}
}
@@ -657,11 +597,6 @@ impl SSEParser for GeminiSSEParser {
}
}
}
// Extract usage metadata
if let Some(usage_metadata) = event.usage_metadata {
self.usage = Some(usage_metadata);
}
}
Ok(())
@@ -692,24 +627,6 @@ pub struct OpenAIUrlCitationEvent {
pub title: Option<String>,
}
/// OpenAI Responses API usage information
#[derive(Deserialize, Debug, Clone)]
pub struct OpenAIResponsesUsage {
#[serde(default)]
pub input_tokens: Option<i32>,
#[serde(default)]
pub output_tokens: Option<i32>,
#[serde(default)]
pub total_tokens: Option<i32>,
}
/// OpenAI Responses API response object (from response.completed event)
#[derive(Deserialize, Debug)]
pub struct OpenAIResponsesResponse {
#[serde(default)]
pub usage: Option<OpenAIResponsesUsage>,
}
/// SSE event types for OpenAI Responses API streaming
/// Based on frontend implementation: openai-responses.ts:220-302
#[derive(Deserialize, Debug)]
@@ -735,10 +652,6 @@ pub enum OpenAIResponsesSSEEvent {
#[serde(rename = "response.done")]
Done {},
/// Response completed with full response object (contains usage)
#[serde(rename = "response.completed")]
Completed { response: OpenAIResponsesResponse },
/// Response created
#[serde(rename = "response.created")]
Created {},
@@ -782,8 +695,6 @@ pub struct OpenAIResponsesSSEParser {
pub annotations: Vec<UrlCitation>,
/// Whether web search was used in this response
pub used_websearch: bool,
/// Token usage from response.completed event
pub usage: Option<OpenAIResponsesUsage>,
}
impl OpenAIResponsesSSEParser {
@@ -797,7 +708,6 @@ impl OpenAIResponsesSSEParser {
stream_event_processor,
annotations: Vec::new(),
used_websearch: false,
usage: None,
}
}
}
@@ -900,13 +810,6 @@ impl SSEParser for OpenAIResponsesSSEParser {
});
}
OpenAIResponsesSSEEvent::Completed { response } => {
// Extract usage from response.completed event
if let Some(usage) = response.usage {
self.usage = Some(usage);
}
}
// Ignore other event types
OpenAIResponsesSSEEvent::Done {}
| OpenAIResponsesSSEEvent::Created {}

View File

@@ -229,85 +229,12 @@ impl ProviderWithResource {
}
}
/// Token usage information from the AI provider
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct TokenUsage {
#[serde(skip_serializing_if = "Option::is_none")]
pub input_tokens: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_tokens: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_tokens: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_read_input_tokens: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_write_input_tokens: Option<i32>,
}
impl TokenUsage {
/// Create a new TokenUsage with basic token counts
pub fn new(input: Option<i32>, output: Option<i32>, total: Option<i32>) -> Self {
Self {
input_tokens: input,
output_tokens: output,
total_tokens: total,
cache_read_input_tokens: None,
cache_write_input_tokens: None,
}
}
/// Create a new TokenUsage with input/output tokens and compute total
pub fn from_input_output(input: Option<i32>, output: Option<i32>) -> Self {
let total = match (input, output) {
(Some(i), Some(o)) => Some(i.saturating_add(o)),
_ => None,
};
Self::new(input, output, total)
}
/// Add cache token information
pub fn with_cache(mut self, read: Option<i32>, write: Option<i32>) -> Self {
self.cache_read_input_tokens = read;
self.cache_write_input_tokens = write;
self
}
pub fn is_empty(&self) -> bool {
self.input_tokens.is_none()
&& self.output_tokens.is_none()
&& self.total_tokens.is_none()
&& self.cache_read_input_tokens.is_none()
&& self.cache_write_input_tokens.is_none()
}
/// Accumulate another TokenUsage into this one (all fields including cache tokens)
/// Uses saturating addition to prevent overflow in long-running agents
pub fn accumulate(&mut self, other: &TokenUsage) {
fn add_option(a: Option<i32>, b: Option<i32>) -> Option<i32> {
match (a, b) {
(Some(x), Some(y)) => Some(x.saturating_add(y)),
(Some(x), None) | (None, Some(x)) => Some(x),
(None, None) => None,
}
}
self.input_tokens = add_option(self.input_tokens, other.input_tokens);
self.output_tokens = add_option(self.output_tokens, other.output_tokens);
self.total_tokens = add_option(self.total_tokens, other.total_tokens);
self.cache_read_input_tokens =
add_option(self.cache_read_input_tokens, other.cache_read_input_tokens);
self.cache_write_input_tokens =
add_option(self.cache_write_input_tokens, other.cache_write_input_tokens);
}
}
#[derive(Serialize)]
pub struct AIAgentResult<'a> {
pub output: Box<RawValue>,
pub messages: Vec<Message<'a>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub wm_stream: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<TokenUsage>,
}
/// Events for streaming AI responses

View File

@@ -305,7 +305,7 @@ pub async fn handle_ai_agent_job(
r#type: "function".to_string(),
function: ToolDefFunction {
name: summary.clone(),
description: Some(summary.clone()),
description: None,
parameters: schema.unwrap_or_else(|| {
to_raw_value(&serde_json::json!({
"type": "object",
@@ -575,7 +575,6 @@ pub async fn run_agent(
let mut actions = vec![];
let mut content = None;
let mut final_usage: Option<crate::ai::types::TokenUsage> = None;
// Check if this provider supports tools with the current output type
let supports_tools = query_builder.supports_tools_with_output_type(output_type);
@@ -720,88 +719,45 @@ pub async fn run_agent(
.await
.0;
// Helper to build HTTP request with headers
let build_http_request = |body: String| {
let mut req = HTTP_CLIENT
.post(&endpoint)
.timeout(timeout)
.header("Content-Type", "application/json");
let mut request = HTTP_CLIENT
.post(&endpoint)
.timeout(timeout)
.header("Content-Type", "application/json");
for (header_name, header_value) in &auth_headers {
req = req.header(*header_name, header_value.clone());
}
// Apply authentication headers
for (header_name, header_value) in &auth_headers {
request = request.header(*header_name, header_value.clone());
}
for (header_name, header_value) in AI_HTTP_HEADERS.iter() {
req = req.header(header_name.as_str(), header_value.as_str());
}
// Apply custom headers from AI_HTTP_HEADERS environment variable
for (header_name, header_value) in AI_HTTP_HEADERS.iter() {
request = request.header(header_name.as_str(), header_value.as_str());
}
req.body(body)
};
let resp = build_http_request(request_body.clone())
let resp = request
.body(request_body)
.send()
.await
.map_err(|e| Error::internal_err(format!("Failed to call API: {}", e)))?;
// Check if request failed and we should retry without stream_options
let resp = match resp.error_for_status_ref() {
Ok(_) => resp,
match resp.error_for_status_ref() {
Ok(_) => {
if let Some(ref stream_event_processor) = stream_event_processor {
query_builder
.parse_streaming_response(resp, stream_event_processor.clone())
.await?
} else {
query_builder.parse_image_response(resp).await?
}
}
Err(e) => {
let status = resp.status();
let _status = resp.status();
let text = resp
.text()
.await
.unwrap_or_else(|_| "<failed to read body>".to_string());
// Retry without stream_options if provider supports it and error suggests incompatibility
// Common error patterns: 400 Bad Request with mentions of stream_options or include_usage
let should_retry = query_builder.supports_retry_without_usage()
&& status.as_u16() == 400
&& (text.contains("stream_options")
|| text.contains("include_usage")
|| text.contains("Additional properties are not allowed"));
if should_retry {
tracing::info!(
"Retrying request without stream_options due to provider incompatibility"
);
let retry_body = query_builder
.build_request_without_usage(&build_args, client, &job.workspace_id)
.await?;
let retry_resp = build_http_request(retry_body)
.send()
.await
.map_err(|e| {
Error::internal_err(format!("Failed to call API on retry: {}", e))
})?;
match retry_resp.error_for_status_ref() {
Ok(_) => retry_resp,
Err(retry_e) => {
let retry_text = retry_resp
.text()
.await
.unwrap_or_else(|_| "<failed to read body>".to_string());
return Err(Error::internal_err(format!(
"API error on retry: {} - {}",
retry_e, retry_text
)));
}
}
} else {
return Err(Error::internal_err(format!("API error: {} - {}", e, text)));
}
return Err(Error::internal_err(format!("API error: {} - {}", e, text)));
}
};
if let Some(ref stream_event_processor) = stream_event_processor {
query_builder
.parse_streaming_response(resp, stream_event_processor.clone())
.await?
} else {
query_builder.parse_image_response(resp).await?
}
};
@@ -812,15 +768,7 @@ pub async fn run_agent(
events_str,
annotations,
used_websearch,
usage,
} => {
// Accumulate usage from this iteration
if let Some(u) = usage {
match &mut final_usage {
Some(existing) => existing.accumulate(&u),
None => final_usage = Some(u),
}
}
if let Some(events_str) = events_str {
final_events_str.push_str(&events_str);
}
@@ -1110,11 +1058,6 @@ pub async fn run_agent(
} else {
None
},
usage: if final_usage.as_ref().map(|u| u.is_empty()).unwrap_or(true) {
None
} else {
final_usage
},
}))
}

View File

@@ -303,7 +303,6 @@ pub async fn handle_dependency_job(
},
deployment_message.clone(),
false,
None,
)
.await
{
@@ -954,10 +953,9 @@ pub async fn handle_flow_dependency_job(
&job.created_by,
&db,
&job.workspace_id,
DeployedObject::Flow { path: job_path, parent_path: parent_path.clone(), version },
DeployedObject::Flow { path: job_path, parent_path, version },
deployment_message,
false,
parent_path.as_deref(),
)
.await
{
@@ -2280,9 +2278,9 @@ pub async fn handle_app_dependency_job(
get_deployment_msg_and_parent_path_from_args(job.args.clone());
let deployed_object = if is_raw_app {
DeployedObject::RawApp { path: job_path, version: id, parent_path: parent_path.clone() }
DeployedObject::RawApp { path: job_path, version: id, parent_path }
} else {
DeployedObject::App { path: job_path, version: id, parent_path: parent_path.clone() }
DeployedObject::App { path: job_path, version: id, parent_path }
};
if let Err(e) = handle_deployment_metadata(
@@ -2293,7 +2291,6 @@ pub async fn handle_app_dependency_job(
deployed_object,
deployment_message,
false,
parent_path.as_deref(),
)
.await
{

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.623.0";
export const VERSION = "v1.621.2";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({

View File

@@ -438,7 +438,6 @@ export async function handleFile(
codebase: await codebase?.getDigest(forceTar),
timeout: typed?.timeout,
on_behalf_of_email: typed?.on_behalf_of_email,
envs: typed?.envs,
};
// console.log(requestBodyCommon.codebase);
@@ -479,8 +478,7 @@ export async function handleFile(
typed.debounce_key == remote["debounce_key"] &&
typed.debounce_delay_s == remote["debounce_delay_s"] &&
typed.codebase == remote.codebase &&
typed.on_behalf_of_email == remote.on_behalf_of_email &&
deepEqual(typed.envs, remote.envs))
typed.on_behalf_of_email == remote.on_behalf_of_email)
) {
log.info(colors.green(`Script ${remotePath} is up to date`));
return true;

View File

@@ -77,7 +77,7 @@ export {
// }
// });
export const VERSION = "1.623.0";
export const VERSION = "1.621.2";
// Re-exported from constants.ts to maintain backwards compatibility
export { WM_FORK_PREFIX } from "./core/constants.ts";

View File

@@ -1,294 +0,0 @@
/**
* Script envs sync test
*
* Tests that script metadata envs field is correctly preserved during sync push.
* Reproduces issue: env variables set in metadata of scripts don't work correctly
* if created by API, then sync pulled, then script modified, then sync push,
* the env variables aren't there anymore.
*/
import { assertEquals, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
import { ensureDir } from "https://deno.land/std@0.224.0/fs/mod.ts";
import { withTestBackend } from "./test_backend.ts";
Deno.test({
name: "Integration: Script envs field is preserved during sync pull/push cycle",
sanitizeResources: false,
sanitizeOps: false,
async fn() {
await withTestBackend(async (backend, tempDir) => {
const uniqueId = Date.now();
const scriptPath = `f/test/envs_script_${uniqueId}`;
// Step 1: Create a script via API with envs set
await ensureDir(`${tempDir}/f/test`);
// Create folder first
const folderResp = await backend.apiRequest!(`/api/w/${backend.workspace}/folders/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "test" }),
});
// Ignore error if folder already exists
// Create a script with envs via API
const createResp = await backend.apiRequest!(`/api/w/${backend.workspace}/scripts/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: scriptPath,
content: `export async function main() {\n return "Hello world";\n}`,
summary: "Test script with envs",
description: "A script to test envs preservation",
language: "bun",
envs: ["MY_ENV_VAR", "ANOTHER_VAR"],
kind: "script",
}),
});
assertEquals(
createResp.ok,
true,
`Failed to create script: ${await createResp.text()}`,
);
// Verify the script was created with envs
const getResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`,
);
const createdScriptText = await getResp.text();
assertEquals(getResp.ok, true, `Failed to get script: ${createdScriptText}`);
const createdScript = JSON.parse(createdScriptText);
assertEquals(
createdScript.envs,
["MY_ENV_VAR", "ANOTHER_VAR"],
"Script should have envs after creation",
);
// Step 2: Create wmill.yaml and sync pull
await Deno.writeTextFile(
`${tempDir}/wmill.yaml`,
`defaultTs: bun
includes:
- "f/test/envs_script_${uniqueId}**"
excludes: []
`,
);
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
assertEquals(
pullResult.code,
0,
`Pull should succeed.\nstdout: ${pullResult.stdout}\nstderr: ${pullResult.stderr}`,
);
// Verify the pulled metadata contains envs
const metadataPath = `${tempDir}/f/test/envs_script_${uniqueId}.script.yaml`;
const metadataContent = await Deno.readTextFile(metadataPath);
assert(
metadataContent.includes("envs:") ||
metadataContent.includes("MY_ENV_VAR") ||
metadataContent.includes("ANOTHER_VAR"),
`Pulled metadata should contain envs. Content:\n${metadataContent}`,
);
// Step 3: Modify the script locally (change content)
const scriptFilePath = `${tempDir}/f/test/envs_script_${uniqueId}.ts`;
const originalContent = await Deno.readTextFile(scriptFilePath);
await Deno.writeTextFile(
scriptFilePath,
originalContent.replace("Hello world", "Hello world modified"),
);
// Step 4: Sync push
const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir);
assertEquals(
pushResult.code,
0,
`Push should succeed.\nstdout: ${pushResult.stdout}\nstderr: ${pushResult.stderr}`,
);
// Step 5: Verify envs are still present on the remote
const getResp2 = await backend.apiRequest!(
`/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`,
);
const updatedScriptText = await getResp2.text();
assertEquals(getResp2.ok, true, `Failed to get script after push: ${updatedScriptText}`);
const updatedScript = JSON.parse(updatedScriptText);
assertEquals(
updatedScript.envs,
["MY_ENV_VAR", "ANOTHER_VAR"],
`Script envs should be preserved after push. Got: ${JSON.stringify(updatedScript.envs)}`,
);
// Also verify the content was updated
assert(
updatedScript.content.includes("Hello world modified"),
"Script content should be updated",
);
});
},
});
Deno.test({
name: "Integration: Script envs field changes are detected and pushed",
sanitizeResources: false,
sanitizeOps: false,
async fn() {
await withTestBackend(async (backend, tempDir) => {
const uniqueId = Date.now();
const scriptPath = `f/test/envs_change_${uniqueId}`;
// Create folder
await ensureDir(`${tempDir}/f/test`);
await backend.apiRequest!(`/api/w/${backend.workspace}/folders/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "test" }),
});
// Create a script with initial envs
const createResp = await backend.apiRequest!(`/api/w/${backend.workspace}/scripts/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: scriptPath,
content: `export async function main() {\n return "Hello";\n}`,
summary: "Test envs change",
description: "",
language: "bun",
envs: ["INITIAL_VAR"],
kind: "script",
}),
});
assertEquals(createResp.ok, true, `Failed to create script: ${await createResp.text()}`);
// Setup wmill.yaml
await Deno.writeTextFile(
`${tempDir}/wmill.yaml`,
`defaultTs: bun
includes:
- "f/test/envs_change_${uniqueId}**"
excludes: []
`,
);
// Pull
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
assertEquals(pullResult.code, 0, `Pull failed: ${pullResult.stderr}`);
// Modify envs in the local metadata file
const metadataPath = `${tempDir}/f/test/envs_change_${uniqueId}.script.yaml`;
let metadataContent = await Deno.readTextFile(metadataPath);
// Replace the envs line(s)
if (metadataContent.includes("envs:")) {
// Replace existing envs
metadataContent = metadataContent.replace(
/envs:\s*\n(\s+-\s+\S+\n?)*/,
"envs:\n - NEW_VAR1\n - NEW_VAR2\n",
);
} else {
// Add envs if not present
metadataContent += "\nenvs:\n - NEW_VAR1\n - NEW_VAR2\n";
}
await Deno.writeTextFile(metadataPath, metadataContent);
// Push
const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir);
assertEquals(pushResult.code, 0, `Push failed: ${pushResult.stderr}`);
// Verify envs were updated on remote
const getResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`,
);
const scriptText = await getResp.text();
assertEquals(getResp.ok, true, `Failed to get script: ${scriptText}`);
const script = JSON.parse(scriptText);
assertEquals(
script.envs,
["NEW_VAR1", "NEW_VAR2"],
`Script envs should be updated to new values. Got: ${JSON.stringify(script.envs)}`,
);
});
},
});
Deno.test({
name: "Integration: Script with empty envs is handled correctly",
sanitizeResources: false,
sanitizeOps: false,
async fn() {
await withTestBackend(async (backend, tempDir) => {
const uniqueId = Date.now();
const scriptPath = `f/test/empty_envs_${uniqueId}`;
// Create folder
await ensureDir(`${tempDir}/f/test`);
await backend.apiRequest!(`/api/w/${backend.workspace}/folders/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "test" }),
});
// Create a script WITHOUT envs
const createResp = await backend.apiRequest!(`/api/w/${backend.workspace}/scripts/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: scriptPath,
content: `export async function main() {\n return "No envs";\n}`,
summary: "Test no envs",
description: "",
language: "bun",
kind: "script",
}),
});
assertEquals(createResp.ok, true, `Failed to create script: ${await createResp.text()}`);
// Setup wmill.yaml
await Deno.writeTextFile(
`${tempDir}/wmill.yaml`,
`defaultTs: bun
includes:
- "f/test/empty_envs_${uniqueId}**"
excludes: []
`,
);
// Pull
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
assertEquals(pullResult.code, 0, `Pull failed: ${pullResult.stderr}`);
// Modify content
const scriptFilePath = `${tempDir}/f/test/empty_envs_${uniqueId}.ts`;
await Deno.writeTextFile(
scriptFilePath,
`export async function main() {\n return "Modified no envs";\n}`,
);
// Push
const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir);
assertEquals(pushResult.code, 0, `Push failed: ${pushResult.stderr}`);
// Verify script was updated and envs is still null/empty
const getResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`,
);
const script = await getResp.json();
assert(
script.content.includes("Modified no envs"),
"Script content should be updated",
);
// envs should be null, empty, or undefined
assert(
!script.envs || script.envs.length === 0,
`Script envs should remain empty. Got: ${JSON.stringify(script.envs)}`,
);
});
},
});

View File

@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.623.0",
"version": "1.621.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.623.0",
"version": "1.621.2",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.623.0",
"version": "1.621.2",
"scripts": {
"dev": "vite dev",
"build": "vite build",

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