Compare commits

..

1 Commits

Author SHA1 Message Date
HugoCasa
8468b2cb42 fix sqlx 2026-01-29 18:50:06 +01:00
108 changed files with 298 additions and 2291 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,22 +1,5 @@
# Changelog
## [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.

94
backend/Cargo.lock generated
View File

@@ -1117,9 +1117,9 @@ dependencies = [
[[package]]
name = "aws-smithy-async"
version = "1.2.9"
version = "1.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e0e99800414b0c4cae85ed775a1559f8992f4e69f5ebafe9c936e29609eae78"
checksum = "9330762ee48c6cecfad2cb37b1506c16c8e858c90638eda2b1a7272b56f88bd5"
dependencies = [
"futures-util",
"pin-project-lite",
@@ -1128,9 +1128,9 @@ dependencies = [
[[package]]
name = "aws-smithy-eventstream"
version = "0.60.16"
version = "0.60.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1adc2eb689a24741c9dcc21ec19be78839a7899594883d3305cf4c7abae9b3d"
checksum = "0810b22ae554f5076c3eabe1fe89b01aee61c354c575789f67e248e83c5f472b"
dependencies = [
"aws-smithy-types",
"bytes",
@@ -1161,9 +1161,9 @@ dependencies = [
[[package]]
name = "aws-smithy-http-client"
version = "1.1.7"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23141f8daeab46574a1969ddb7316bc2928732e5721b3abfa8d1e16927ea9a52"
checksum = "ec918f18147cec121cb142a91b0038f66d99bbe903e585dccf871920e90b22ab"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
@@ -1200,18 +1200,18 @@ dependencies = [
[[package]]
name = "aws-smithy-observability"
version = "0.2.2"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112e30b3c5379273de88c8cedfc96ce0211e9af22115ceb6975b5c072eccdfb9"
checksum = "a700a7702874cd78b85fecdc9f64f3f72eb22fb713791cb445bcfd2a15bc1ecf"
dependencies = [
"aws-smithy-runtime-api",
]
[[package]]
name = "aws-smithy-query"
version = "0.60.11"
version = "0.60.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a57c5122eafc566cba4d3cbaacb53dd8a0cacd71155c728d1f4a9179cdd75ae"
checksum = "adc4a6cdc289a37be7fddb7f4365448187d62c603a40e6d46d13c68e5e81900f"
dependencies = [
"aws-smithy-types",
"urlencoding",
@@ -1243,9 +1243,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime-api"
version = "1.11.1"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d09ba34c17c65a53b65b0ec0d80cf7a934947d407cb7c4fb7753f96de147663"
checksum = "5c47b1e62accf759b01aba295e40479d1ba8fb77c2a54f0fed861c809ca49761"
dependencies = [
"aws-smithy-async",
"aws-smithy-types",
@@ -1260,9 +1260,9 @@ dependencies = [
[[package]]
name = "aws-smithy-types"
version = "1.4.1"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8169129deda9dc18731b7b160f75121507e02f45f2101e48f0252dcd997e9da1"
checksum = "c2d447863bdec38c899e5753a48c0abcf590f3ec629e257ad5a9ef8806ad7714"
dependencies = [
"base64-simd 0.8.0",
"bytes",
@@ -15466,7 +15466,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"aws-sdk-config",
@@ -15529,7 +15529,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"argon2",
@@ -15659,7 +15659,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -15669,7 +15669,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"chrono",
"lazy_static",
@@ -15683,7 +15683,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -15702,7 +15702,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"async-recursion",
@@ -15798,7 +15798,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"regex",
"serde",
@@ -15813,7 +15813,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -15837,7 +15837,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -15853,7 +15853,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"async-trait",
@@ -15873,7 +15873,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"async-oauth2",
@@ -15897,7 +15897,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -15906,7 +15906,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -15918,7 +15918,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"serde_json",
@@ -15930,7 +15930,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"gosyn",
@@ -15942,7 +15942,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -15954,7 +15954,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"serde_json",
@@ -15966,7 +15966,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"nu-parser",
@@ -15977,7 +15977,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15988,7 +15988,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16001,7 +16001,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"async-recursion",
@@ -16025,7 +16025,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -16039,7 +16039,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -16056,7 +16056,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -16070,7 +16070,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -16089,7 +16089,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"serde",
@@ -16100,7 +16100,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"async-recursion",
@@ -16137,7 +16137,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -16147,7 +16147,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.622.0"
version = "1.621.2"
dependencies = [
"anyhow",
"async-once-cell",
@@ -17048,18 +17048,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.36"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc"
checksum = "fdea86ddd5568519879b8187e1cf04e24fce28f7fe046ceecbce472ff19a2572"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.36"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a"
checksum = "0c15e1b46eff7c6c91195752e0eeed8ef040e391cdece7c25376957d5f15df22"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.622.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.622.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.622.0
version: 1.621.2
title: Windmill API
contact:
@@ -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)

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

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

View File

@@ -77,7 +77,7 @@ export {
// }
// });
export const VERSION = "1.622.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,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.622.0",
"version": "1.621.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.622.0",
"version": "1.621.2",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {

View File

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

View File

@@ -109,10 +109,6 @@
} else if (kind === 'app') {
const app = await AppService.getAppByPath({ workspace, path })
return app.summary
} else if (kind === 'folder') {
const folder = await FolderService.getFolder({ workspace, name: path.slice(2) })
return folder.summary
}
} catch (error) {
console.error(`Failed to fetch summary for ${kind}:${path}`, error)
@@ -122,7 +118,7 @@
async function fetchSummaries(diffs: WorkspaceItemDiff[]) {
// Only fetch summaries for scripts, flows, and apps
const itemsToFetch = diffs.filter((diff) => ['script', 'flow', 'app', 'folder'].includes(diff.kind))
const itemsToFetch = diffs.filter((diff) => ['script', 'flow', 'app'].includes(diff.kind))
for (const diff of itemsToFetch) {
const key = getItemKey(diff)
@@ -322,33 +318,33 @@
workspace: workspace,
name: path
})
// } else if (kind === 'trigger') {
// const triggersKind: TriggerKind[] = [
// 'kafka',
// 'mqtt',
// 'nats',
// 'postgres',
// 'routes',
// 'schedules',
// 'sqs',
// 'websockets',
// 'gcp'
// ]
// if (
// additionalInformation?.triggers &&
// triggersKind.includes(additionalInformation.triggers.kind)
// ) {
// exists = await existsTrigger(
// { workspace: workspace, path },
// additionalInformation.triggers.kind
// )
// } else {
// throw new Error(
// `Unexpected triggers kind, expected one of: '${triggersKind.join(', ')}' got: ${
// additionalInformation?.triggers?.kind
// }`
// )
// }
// } else if (kind === 'trigger') {
// const triggersKind: TriggerKind[] = [
// 'kafka',
// 'mqtt',
// 'nats',
// 'postgres',
// 'routes',
// 'schedules',
// 'sqs',
// 'websockets',
// 'gcp'
// ]
// if (
// additionalInformation?.triggers &&
// triggersKind.includes(additionalInformation.triggers.kind)
// ) {
// exists = await existsTrigger(
// { workspace: workspace, path },
// additionalInformation.triggers.kind
// )
// } else {
// throw new Error(
// `Unexpected triggers kind, expected one of: '${triggersKind.join(', ')}' got: ${
// additionalInformation?.triggers?.kind
// }`
// )
// }
} else {
throw new Error(`Unknown kind ${kind}`)
}
@@ -859,8 +855,7 @@
This fork is ahead of its parent
{/if}
and some of the changes are not visible by you. Only a user with access to the whole context
may deploy or update this fork. You can share the link to this page to someone with proper permissions
to get it deployed.
may deploy or update this fork. You can share the link to this page to someone with proper permissions to get it deployed.
</Alert>
{/if}
@@ -906,11 +901,7 @@
disabled={!isSelectable}
selected={isSelected && !(deploymentStatus[key]?.status == 'deployed')}
onSelect={() => toggleItem(diff)}
path={diff.kind != 'resource' &&
diff.kind != 'variable' &&
diff.kind != 'resource_type'
? diff.path
: ''}
path={diff.kind != 'resource' && diff.kind != 'variable' ? diff.path : ''}
marked={undefined}
kind={diff.kind}
canFavorite={false}

View File

@@ -150,22 +150,6 @@
: ''}
</span>
{/if}
{#if comparison.summary.resource_types_changed > 0}
<span class="text-blue-700 dark:text-blue-100">
{comparison.summary.resource_types_changed} resource type{comparison.summary
.resource_types_changed !== 1
? 's'
: ''}
</span>
{/if}
{#if comparison.summary.folders_changed > 0}
<span class="text-blue-700 dark:text-blue-100">
{comparison.summary.folders_changed} folder{comparison.summary
.folders_changed !== 1
? 's'
: ''}
</span>
{/if}
</div>
{#if comparison.summary.conflicts > 0}

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
import { Boxes, Code2, DollarSign, Folder, LayoutDashboard } from 'lucide-svelte'
import { Boxes, Code2, DollarSign, LayoutDashboard } from 'lucide-svelte'
export let kind: 'script' | 'flow' | 'app' | 'raw_app' | 'resource' | 'variable' | 'resource_type' | 'folder'
export let kind: 'script' | 'flow' | 'app' | 'raw_app' | 'resource' | 'variable'
</script>
<div class="flex justify-center items-center" title={kind}>
<div class="flex justify-center items-center">
{#if kind === 'flow'}
<BarsStaggered size={16} class="text-teal-500" />
{:else if kind === 'app' || kind === 'raw_app'}
@@ -16,10 +16,6 @@
<DollarSign size={16} class="text-gray-400" />
{:else if kind === 'resource'}
<Boxes size={16} class="text-gray-400" />
{:else if kind === 'resource_type'}
<div style="width: 16px; height: 16px;" class="bg-gray-100 rounded-full" ></div>
{:else if kind === 'folder'}
<Folder size={16} class="text-gray-400" />
{:else}
<div class="w-[16px]"></div>
{/if}

View File

@@ -14,7 +14,7 @@ import Anthropic from '@anthropic-ai/sdk'
import { get, type Writable } from 'svelte/store'
import { OpenAPI, ResourceService, type Script } from '../../gen'
import { EDIT_CONFIG, FIX_CONFIG, GEN_CONFIG } from './prompts'
import { formatResourceTypes } from './utils'
import { formatResourceTypes, isMistralFamily } from './utils'
import { z } from 'zod'
import { processToolCall, type Tool, type ToolCallbacks } from './chat/shared'
import {
@@ -290,6 +290,7 @@ function getModelSpecificConfig(
const modelKey = `${modelProvider.provider}:${modelProvider.model}`
const customMaxTokensStore = get(copilotInfo)?.maxTokensPerModel
const maxTokens = customMaxTokensStore?.[modelKey] ?? defaultMaxTokens
const isMistralModel = isMistralFamily(modelProvider.model)
if (
(modelProvider.provider === 'openai' || modelProvider.provider === 'azure_openai') &&
(modelProvider.model.startsWith('o') || modelProvider.model.startsWith('gpt-5'))
@@ -297,7 +298,8 @@ function getModelSpecificConfig(
return {
model: modelProvider.model,
...(tools && tools.length > 0 ? { tools } : {}),
max_completion_tokens: maxTokens
max_completion_tokens: maxTokens,
...(isMistralModel ? { seed: undefined } : {})
}
} else {
return {
@@ -314,7 +316,8 @@ function getModelSpecificConfig(
temperature: 0
}),
...(tools && tools.length > 0 ? { tools } : {}),
max_tokens: maxTokens
max_tokens: maxTokens,
...(isMistralModel ? { seed: undefined } : {})
}
}
}
@@ -346,6 +349,7 @@ function prepareMessages(aiProvider: AIProvider, messages: ChatCompletionMessage
const DEFAULT_COMPLETION_CONFIG: ChatCompletionCreateParams = {
model: '',
seed: 42,
messages: []
}
@@ -357,8 +361,14 @@ export const PROVIDER_COMPLETION_CONFIG_MAP: Record<AIProvider, ChatCompletionCr
togetherai: DEFAULT_COMPLETION_CONFIG,
deepseek: DEFAULT_COMPLETION_CONFIG,
customai: DEFAULT_COMPLETION_CONFIG,
googleai: DEFAULT_COMPLETION_CONFIG,
mistral: DEFAULT_COMPLETION_CONFIG,
googleai: {
...DEFAULT_COMPLETION_CONFIG,
seed: undefined // not supported by gemini
} as ChatCompletionCreateParams,
mistral: {
...DEFAULT_COMPLETION_CONFIG,
seed: undefined
},
anthropic: DEFAULT_COMPLETION_CONFIG,
aws_bedrock: DEFAULT_COMPLETION_CONFIG
} as const

View File

@@ -178,3 +178,11 @@ export function supportsAutocomplete(model: string): boolean {
return lower.includes('codestral') && !lower.includes('embed')
}
/**
* Checks if a model belongs to the Mistral family.
* Used for provider-specific configurations (e.g., excluding seed parameter).
*/
export function isMistralFamily(model: string): boolean {
const lower = model.toLowerCase()
return lower.includes('mistral') || lower.includes('codestral')
}

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