Compare commits
1 Commits
v1.623.0
...
hc/fix-sql
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8468b2cb42 |
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
30
CHANGELOG.md
30
CHANGELOG.md
@@ -1,35 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [1.623.0](https://github.com/windmill-labs/windmill/compare/v1.622.0...v1.623.0) (2026-01-31)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add PocketID OAuth provider support ([#7318](https://github.com/windmill-labs/windmill/issues/7318)) ([720e3c5](https://github.com/windmill-labs/windmill/commit/720e3c543623c2612b1af704c13d032c53368efb))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add schema compatibility layer for MCP clients like n8n ([#7747](https://github.com/windmill-labs/windmill/issues/7747)) ([297aa23](https://github.com/windmill-labs/windmill/commit/297aa23ed46315dfd4b034d44361a5bd8aaca884))
|
||||
* preserve script envs field during sync push ([f405dff](https://github.com/windmill-labs/windmill/commit/f405dff2e22681dc8d4f3a9b7427e278c6cfb0cc))
|
||||
|
||||
## [1.622.0](https://github.com/windmill-labs/windmill/compare/v1.621.2...v1.622.0) (2026-01-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add token usage tracking to AI agent output ([#7738](https://github.com/windmill-labs/windmill/issues/7738)) ([ce23f21](https://github.com/windmill-labs/windmill/commit/ce23f21c0e0bc6365f616ace4c45fa341741c555))
|
||||
* workspace dedicated workers ([#7741](https://github.com/windmill-labs/windmill/issues/7741)) ([60858d1](https://github.com/windmill-labs/windmill/commit/60858d1e20e68b83fddcdbfc0ff34decaff5d1c5))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* forward teams error to client ([#7746](https://github.com/windmill-labs/windmill/issues/7746)) ([ca8dbc0](https://github.com/windmill-labs/windmill/commit/ca8dbc0676dda619aff6fab7f6ff05ed773738e0))
|
||||
* indexer build error ([#7744](https://github.com/windmill-labs/windmill/issues/7744)) ([6679ecb](https://github.com/windmill-labs/windmill/commit/6679ecb9a2ead08d2252a64f2a27a6d539fa23e9))
|
||||
* remove uuid-ossp extension requirement for RDS compatibility ([ad5293c](https://github.com/windmill-labs/windmill/commit/ad5293c0edacfaf1431a3639ef5ea32d9bd761b0))
|
||||
* require AGENT_TOKEN and BASE_INTERNAL_URL for agent mode ([6c84a89](https://github.com/windmill-labs/windmill/commit/6c84a8905382e29a4bbe0ae947eda794bc4dc566))
|
||||
* visibility bug on deployment UI (issue when renaming items) + add tracking of folders and resource types ([#7739](https://github.com/windmill-labs/windmill/issues/7739)) ([998f11a](https://github.com/windmill-labs/windmill/commit/998f11a10da45c6d933d8b78ca24ed4f55a53f3b))
|
||||
|
||||
## [1.621.2](https://github.com/windmill-labs/windmill/compare/v1.621.1...v1.621.2) (2026-01-29)
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
16
backend/.sqlx/query-c9c040ec228a8fe4fda08439420141bee63339d4e3d5e2d68aabb12009f691c6.json
generated
Normal file
16
backend/.sqlx/query-c9c040ec228a8fe4fda08439420141bee63339d4e3d5e2d68aabb12009f691c6.json
generated
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -11,11 +11,10 @@ Windmill uses a workspace-based architecture with multiple crates:
|
||||
- **windmill-audit**: Audit logging
|
||||
- Other specialized crates (git-sync, autoscaling, etc.)
|
||||
|
||||
## Key References (MUST FOLLOW THESE)
|
||||
## Key References
|
||||
|
||||
- You MUST follow best-practices by using the `rust-backend` skill, everytime you write RUST code.
|
||||
- When working with the database: read `summarized_schema.txt` before starting
|
||||
- When working with the API routes: you can read `windmill-api/src/lib.rs` to get started
|
||||
- Database schema: @summarized_schema.txt
|
||||
- API route prefixes: `windmill-api/src/lib.rs`
|
||||
|
||||
## Adding New Code
|
||||
|
||||
@@ -58,4 +57,8 @@ Windmill uses a workspace-based architecture with multiple crates:
|
||||
- **sqlx**: Database operations
|
||||
- **serde**: Serialization/deserialization
|
||||
- **tracing**: Logging and diagnostics
|
||||
- **reqwest**: HTTP client
|
||||
- **reqwest**: HTTP client
|
||||
|
||||
## Coding Guidelines
|
||||
|
||||
Detailed Rust coding patterns and best practices are provided by the `rust-backend` skill.
|
||||
114
backend/Cargo.lock
generated
114
backend/Cargo.lock
generated
@@ -1117,9 +1117,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-async"
|
||||
version = "1.2.10"
|
||||
version = "1.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e39f47abe8641e434de98e047e85ced629862e7ab719b6914a846796ceb289e2"
|
||||
checksum = "9330762ee48c6cecfad2cb37b1506c16c8e858c90638eda2b1a7272b56f88bd5"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
@@ -1128,9 +1128,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-eventstream"
|
||||
version = "0.60.17"
|
||||
version = "0.60.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff6a1db93c9aaf8c8e5d97f055e5fa01a782bd5bdc9c4042b7e18090503b12a7"
|
||||
checksum = "0810b22ae554f5076c3eabe1fe89b01aee61c354c575789f67e248e83c5f472b"
|
||||
dependencies = [
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
@@ -1161,9 +1161,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-http-client"
|
||||
version = "1.1.8"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a395c914b1ff95db3cb7003ea4fd19432343af698a9b5028a7d35f8e712240a1"
|
||||
checksum = "ec918f18147cec121cb142a91b0038f66d99bbe903e585dccf871920e90b22ab"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-runtime-api",
|
||||
@@ -1200,18 +1200,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-observability"
|
||||
version = "0.2.3"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7764bc1dfdb71157bc481528a649e617ed8c9c8aa93c0e8b01087133677cfc8e"
|
||||
checksum = "a700a7702874cd78b85fecdc9f64f3f72eb22fb713791cb445bcfd2a15bc1ecf"
|
||||
dependencies = [
|
||||
"aws-smithy-runtime-api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-query"
|
||||
version = "0.60.12"
|
||||
version = "0.60.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "786bbf4434ed3e3413c5a1741abf53a0372dd48124ddb2091e6bff1b1b2582b5"
|
||||
checksum = "adc4a6cdc289a37be7fddb7f4365448187d62c603a40e6d46d13c68e5e81900f"
|
||||
dependencies = [
|
||||
"aws-smithy-types",
|
||||
"urlencoding",
|
||||
@@ -1243,9 +1243,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-runtime-api"
|
||||
version = "1.11.2"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b743e9aab0b8d50a9a40eebedf974fcfe3621032e07c6388d1c7821b155b7b0"
|
||||
checksum = "5c47b1e62accf759b01aba295e40479d1ba8fb77c2a54f0fed861c809ca49761"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-types",
|
||||
@@ -1260,9 +1260,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-types"
|
||||
version = "1.4.2"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0828575b70da70406b4cdb5d4afe0afe725f72245f04d34f02e0fb5ebd6fc872"
|
||||
checksum = "c2d447863bdec38c899e5753a48c0abcf590f3ec629e257ad5a9ef8806ad7714"
|
||||
dependencies = [
|
||||
"base64-simd 0.8.0",
|
||||
"bytes",
|
||||
@@ -1976,9 +1976,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.0"
|
||||
version = "1.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
||||
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
|
||||
dependencies = [
|
||||
"bytemuck_derive",
|
||||
]
|
||||
@@ -2150,9 +2150,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.55"
|
||||
version = "1.2.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
|
||||
checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@@ -5566,9 +5566,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
@@ -10054,9 +10054,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.13.1"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||
checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950"
|
||||
|
||||
[[package]]
|
||||
name = "postgres-native-tls"
|
||||
@@ -12399,9 +12399,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
|
||||
|
||||
[[package]]
|
||||
name = "slotmap"
|
||||
@@ -15466,7 +15466,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"aws-sdk-config",
|
||||
@@ -15529,7 +15529,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -15659,7 +15659,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
@@ -15669,7 +15669,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
@@ -15683,7 +15683,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -15702,7 +15702,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15798,7 +15798,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -15813,7 +15813,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -15837,7 +15837,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -15853,7 +15853,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-mcp"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15873,7 +15873,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-oauth"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-oauth2",
|
||||
@@ -15897,7 +15897,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -15906,7 +15906,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15918,7 +15918,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15930,7 +15930,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -15942,7 +15942,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15954,7 +15954,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15966,7 +15966,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -15977,7 +15977,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -15988,7 +15988,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -16001,7 +16001,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -16025,7 +16025,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16039,7 +16039,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -16056,7 +16056,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16070,7 +16070,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16089,7 +16089,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -16100,7 +16100,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -16137,7 +16137,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
@@ -16147,7 +16147,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-once-cell",
|
||||
@@ -17048,18 +17048,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.37"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac"
|
||||
checksum = "fdea86ddd5568519879b8187e1cf04e24fce28f7fe046ceecbce472ff19a2572"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.37"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0"
|
||||
checksum = "0c15e1b46eff7c6c91195752e0eeed8ef040e391cdece7c25376957d5f15df22"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -35,7 +35,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.623.0"
|
||||
version = "1.621.2"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
fa881b63272aebab8ef79d262be7da2a2908c227
|
||||
a18ac31062ac092cb9a5fc87629e217d97f4911d
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
-- Add down migration script here
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Add up migration script here
|
||||
UPDATE workspace_diff SET has_changes = NULL;
|
||||
@@ -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,
|
||||
|
||||
@@ -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}")),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.623.0
|
||||
version: 1.621.2
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -18121,6 +18121,7 @@ components:
|
||||
- visible_to_owner
|
||||
- tag
|
||||
|
||||
|
||||
ExportableCompletedJob:
|
||||
type: object
|
||||
description: Completed job with full data for export/import operations
|
||||
@@ -22275,8 +22276,7 @@ components:
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
enum:
|
||||
["script", "flow", "app", "resource", "variable", "resource_type"]
|
||||
enum: ["script", "flow", "app", "resource", "variable"]
|
||||
description: Type of the item
|
||||
path:
|
||||
type: string
|
||||
@@ -22308,8 +22308,6 @@ components:
|
||||
- apps_changed
|
||||
- resources_changed
|
||||
- variables_changed
|
||||
- resource_types_changed
|
||||
- folders_changed
|
||||
- conflicts
|
||||
properties:
|
||||
total_diffs:
|
||||
@@ -22336,12 +22334,6 @@ components:
|
||||
variables_changed:
|
||||
type: integer
|
||||
description: Number of variables with differences
|
||||
resource_types_changed:
|
||||
type: integer
|
||||
description: Number of resource types with differences
|
||||
folders_changed:
|
||||
type: integer
|
||||
description: Number of folders with differences
|
||||
conflicts:
|
||||
type: integer
|
||||
description: Number of items that are both ahead and behind (conflicts)
|
||||
@@ -22588,13 +22580,14 @@ components:
|
||||
type: object
|
||||
properties:
|
||||
service_name:
|
||||
$ref: "#/components/schemas/NativeServiceName"
|
||||
$ref: '#/components/schemas/NativeServiceName'
|
||||
oauth_data:
|
||||
nullable: true
|
||||
$ref: "#/components/schemas/WorkspaceOAuthConfig"
|
||||
$ref: '#/components/schemas/WorkspaceOAuthConfig'
|
||||
required:
|
||||
- service_name
|
||||
|
||||
|
||||
WorkspaceOAuthConfig:
|
||||
type: object
|
||||
properties:
|
||||
@@ -22625,7 +22618,7 @@ components:
|
||||
type: string
|
||||
enum: [webhook]
|
||||
request_type:
|
||||
$ref: "#/components/schemas/WebhookRequestType"
|
||||
$ref: '#/components/schemas/WebhookRequestType'
|
||||
required:
|
||||
- type
|
||||
- request_type
|
||||
@@ -22641,10 +22634,12 @@ components:
|
||||
type: object
|
||||
properties:
|
||||
redirect_uri:
|
||||
type: string
|
||||
type:
|
||||
string
|
||||
required:
|
||||
- redirect_uri
|
||||
|
||||
|
||||
NativeTriggerData:
|
||||
type: object
|
||||
description: Data for creating or updating a native trigger
|
||||
|
||||
@@ -1391,7 +1391,6 @@ async fn delete_app(
|
||||
deployed_object,
|
||||
Some(format!("App '{}' deleted", path)),
|
||||
true,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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?
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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()))?;
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(());
|
||||
|
||||
@@ -39,63 +39,3 @@ pub fn extract_resource_types_from_schema(schema: &SchemaType) -> HashSet<String
|
||||
}
|
||||
resource_types
|
||||
}
|
||||
|
||||
/// Transform a JSON schema for maximum MCP client compatibility.
|
||||
///
|
||||
/// Some MCP clients (e.g., n8n) have limited JSON Schema support:
|
||||
/// - `integer` type is not supported (convert to `number`)
|
||||
pub fn make_schema_compatible(schema: &mut Value) {
|
||||
let Value::Object(obj) = schema else { return };
|
||||
|
||||
// 1. Convert integer to number
|
||||
if let Some(type_val) = obj.get_mut("type") {
|
||||
match type_val {
|
||||
Value::String(s) if s == "integer" => *s = "number".to_string(),
|
||||
Value::Array(arr) => {
|
||||
for item in arr.iter_mut() {
|
||||
if let Value::String(s) = item {
|
||||
if s == "integer" {
|
||||
*s = "number".to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process nested schemas
|
||||
if let Some(Value::Object(props)) = obj.get_mut("properties") {
|
||||
for value in props.values_mut() {
|
||||
make_schema_compatible(value);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(items) = obj.get_mut("items") {
|
||||
make_schema_compatible(items);
|
||||
}
|
||||
|
||||
if let Some(additional) = obj.get_mut("additionalProperties") {
|
||||
if additional.is_object() {
|
||||
make_schema_compatible(additional);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Value::Array(all_of)) = obj.get_mut("allOf") {
|
||||
for s in all_of.iter_mut() {
|
||||
make_schema_compatible(s);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Value::Array(one_of)) = obj.get_mut("oneOf") {
|
||||
for s in one_of.iter_mut() {
|
||||
make_schema_compatible(s);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Value::Array(any_of)) = obj.get_mut("anyOf") {
|
||||
for s in any_of.iter_mut() {
|
||||
make_schema_compatible(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::common::schema::make_schema_compatible;
|
||||
|
||||
/// Represents an auto-generated endpoint tool from OpenAPI specification
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct EndpointTool {
|
||||
@@ -39,12 +37,11 @@ pub fn endpoint_tool_to_mcp_tool(tool: &EndpointTool) -> Tool {
|
||||
merge_schema_into(&mut combined_properties, &mut combined_required, schema);
|
||||
}
|
||||
|
||||
let mut combined_schema = serde_json::json!({
|
||||
let combined_schema = serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": combined_properties,
|
||||
"required": combined_required
|
||||
});
|
||||
make_schema_compatible(&mut combined_schema);
|
||||
|
||||
let description = format!("{}. {}", tool.description, tool.instructions);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::common::schema::{convert_schema_to_schema_type, make_schema_compatible};
|
||||
use crate::common::schema::convert_schema_to_schema_type;
|
||||
use crate::common::transform::transform_path;
|
||||
use crate::common::types::{
|
||||
FlowInfo, HubScriptInfo, ResourceInfo, ResourceType, SchemaType, ScriptInfo, ToolableItem,
|
||||
@@ -147,18 +147,13 @@ pub fn create_tool_from_item<T: ToolableItem, B: McpBackend>(
|
||||
backend.transform_schema_for_resources(&schema, resources_cache, resources_types);
|
||||
|
||||
let input_schema_map = match serde_json::to_value(schema_obj) {
|
||||
Ok(mut value) => {
|
||||
make_schema_compatible(&mut value);
|
||||
match value {
|
||||
serde_json::Value::Object(map) => map,
|
||||
_ => {
|
||||
tracing::warn!(
|
||||
"Schema object for tool '{}' did not serialize to a JSON object, using empty schema.",
|
||||
path
|
||||
);
|
||||
serde_json::Map::new()
|
||||
}
|
||||
}
|
||||
Ok(serde_json::Value::Object(map)) => map,
|
||||
Ok(_) => {
|
||||
tracing::warn!(
|
||||
"Schema object for tool '{}' did not serialize to a JSON object, using empty schema.",
|
||||
path
|
||||
);
|
||||
serde_json::Map::new()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.623.0";
|
||||
export const VERSION = "v1.621.2";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
@@ -438,7 +438,6 @@ export async function handleFile(
|
||||
codebase: await codebase?.getDigest(forceTar),
|
||||
timeout: typed?.timeout,
|
||||
on_behalf_of_email: typed?.on_behalf_of_email,
|
||||
envs: typed?.envs,
|
||||
};
|
||||
|
||||
// console.log(requestBodyCommon.codebase);
|
||||
@@ -479,8 +478,7 @@ export async function handleFile(
|
||||
typed.debounce_key == remote["debounce_key"] &&
|
||||
typed.debounce_delay_s == remote["debounce_delay_s"] &&
|
||||
typed.codebase == remote.codebase &&
|
||||
typed.on_behalf_of_email == remote.on_behalf_of_email &&
|
||||
deepEqual(typed.envs, remote.envs))
|
||||
typed.on_behalf_of_email == remote.on_behalf_of_email)
|
||||
) {
|
||||
log.info(colors.green(`Script ${remotePath} is up to date`));
|
||||
return true;
|
||||
|
||||
@@ -77,7 +77,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.623.0";
|
||||
export const VERSION = "1.621.2";
|
||||
|
||||
// Re-exported from constants.ts to maintain backwards compatibility
|
||||
export { WM_FORK_PREFIX } from "./core/constants.ts";
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
/**
|
||||
* Script envs sync test
|
||||
*
|
||||
* Tests that script metadata envs field is correctly preserved during sync push.
|
||||
* Reproduces issue: env variables set in metadata of scripts don't work correctly
|
||||
* if created by API, then sync pulled, then script modified, then sync push,
|
||||
* the env variables aren't there anymore.
|
||||
*/
|
||||
|
||||
import { assertEquals, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { ensureDir } from "https://deno.land/std@0.224.0/fs/mod.ts";
|
||||
import { withTestBackend } from "./test_backend.ts";
|
||||
|
||||
Deno.test({
|
||||
name: "Integration: Script envs field is preserved during sync pull/push cycle",
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
async fn() {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
const uniqueId = Date.now();
|
||||
const scriptPath = `f/test/envs_script_${uniqueId}`;
|
||||
|
||||
// Step 1: Create a script via API with envs set
|
||||
await ensureDir(`${tempDir}/f/test`);
|
||||
|
||||
// Create folder first
|
||||
const folderResp = await backend.apiRequest!(`/api/w/${backend.workspace}/folders/create`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: "test" }),
|
||||
});
|
||||
// Ignore error if folder already exists
|
||||
|
||||
// Create a script with envs via API
|
||||
const createResp = await backend.apiRequest!(`/api/w/${backend.workspace}/scripts/create`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
path: scriptPath,
|
||||
content: `export async function main() {\n return "Hello world";\n}`,
|
||||
summary: "Test script with envs",
|
||||
description: "A script to test envs preservation",
|
||||
language: "bun",
|
||||
envs: ["MY_ENV_VAR", "ANOTHER_VAR"],
|
||||
kind: "script",
|
||||
}),
|
||||
});
|
||||
|
||||
assertEquals(
|
||||
createResp.ok,
|
||||
true,
|
||||
`Failed to create script: ${await createResp.text()}`,
|
||||
);
|
||||
|
||||
// Verify the script was created with envs
|
||||
const getResp = await backend.apiRequest!(
|
||||
`/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`,
|
||||
);
|
||||
const createdScriptText = await getResp.text();
|
||||
assertEquals(getResp.ok, true, `Failed to get script: ${createdScriptText}`);
|
||||
const createdScript = JSON.parse(createdScriptText);
|
||||
assertEquals(
|
||||
createdScript.envs,
|
||||
["MY_ENV_VAR", "ANOTHER_VAR"],
|
||||
"Script should have envs after creation",
|
||||
);
|
||||
|
||||
// Step 2: Create wmill.yaml and sync pull
|
||||
await Deno.writeTextFile(
|
||||
`${tempDir}/wmill.yaml`,
|
||||
`defaultTs: bun
|
||||
includes:
|
||||
- "f/test/envs_script_${uniqueId}**"
|
||||
excludes: []
|
||||
`,
|
||||
);
|
||||
|
||||
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
|
||||
assertEquals(
|
||||
pullResult.code,
|
||||
0,
|
||||
`Pull should succeed.\nstdout: ${pullResult.stdout}\nstderr: ${pullResult.stderr}`,
|
||||
);
|
||||
|
||||
// Verify the pulled metadata contains envs
|
||||
const metadataPath = `${tempDir}/f/test/envs_script_${uniqueId}.script.yaml`;
|
||||
const metadataContent = await Deno.readTextFile(metadataPath);
|
||||
assert(
|
||||
metadataContent.includes("envs:") ||
|
||||
metadataContent.includes("MY_ENV_VAR") ||
|
||||
metadataContent.includes("ANOTHER_VAR"),
|
||||
`Pulled metadata should contain envs. Content:\n${metadataContent}`,
|
||||
);
|
||||
|
||||
// Step 3: Modify the script locally (change content)
|
||||
const scriptFilePath = `${tempDir}/f/test/envs_script_${uniqueId}.ts`;
|
||||
const originalContent = await Deno.readTextFile(scriptFilePath);
|
||||
await Deno.writeTextFile(
|
||||
scriptFilePath,
|
||||
originalContent.replace("Hello world", "Hello world modified"),
|
||||
);
|
||||
|
||||
// Step 4: Sync push
|
||||
const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir);
|
||||
assertEquals(
|
||||
pushResult.code,
|
||||
0,
|
||||
`Push should succeed.\nstdout: ${pushResult.stdout}\nstderr: ${pushResult.stderr}`,
|
||||
);
|
||||
|
||||
// Step 5: Verify envs are still present on the remote
|
||||
const getResp2 = await backend.apiRequest!(
|
||||
`/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`,
|
||||
);
|
||||
const updatedScriptText = await getResp2.text();
|
||||
assertEquals(getResp2.ok, true, `Failed to get script after push: ${updatedScriptText}`);
|
||||
const updatedScript = JSON.parse(updatedScriptText);
|
||||
|
||||
assertEquals(
|
||||
updatedScript.envs,
|
||||
["MY_ENV_VAR", "ANOTHER_VAR"],
|
||||
`Script envs should be preserved after push. Got: ${JSON.stringify(updatedScript.envs)}`,
|
||||
);
|
||||
|
||||
// Also verify the content was updated
|
||||
assert(
|
||||
updatedScript.content.includes("Hello world modified"),
|
||||
"Script content should be updated",
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "Integration: Script envs field changes are detected and pushed",
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
async fn() {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
const uniqueId = Date.now();
|
||||
const scriptPath = `f/test/envs_change_${uniqueId}`;
|
||||
|
||||
// Create folder
|
||||
await ensureDir(`${tempDir}/f/test`);
|
||||
await backend.apiRequest!(`/api/w/${backend.workspace}/folders/create`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: "test" }),
|
||||
});
|
||||
|
||||
// Create a script with initial envs
|
||||
const createResp = await backend.apiRequest!(`/api/w/${backend.workspace}/scripts/create`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
path: scriptPath,
|
||||
content: `export async function main() {\n return "Hello";\n}`,
|
||||
summary: "Test envs change",
|
||||
description: "",
|
||||
language: "bun",
|
||||
envs: ["INITIAL_VAR"],
|
||||
kind: "script",
|
||||
}),
|
||||
});
|
||||
assertEquals(createResp.ok, true, `Failed to create script: ${await createResp.text()}`);
|
||||
|
||||
// Setup wmill.yaml
|
||||
await Deno.writeTextFile(
|
||||
`${tempDir}/wmill.yaml`,
|
||||
`defaultTs: bun
|
||||
includes:
|
||||
- "f/test/envs_change_${uniqueId}**"
|
||||
excludes: []
|
||||
`,
|
||||
);
|
||||
|
||||
// Pull
|
||||
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
|
||||
assertEquals(pullResult.code, 0, `Pull failed: ${pullResult.stderr}`);
|
||||
|
||||
// Modify envs in the local metadata file
|
||||
const metadataPath = `${tempDir}/f/test/envs_change_${uniqueId}.script.yaml`;
|
||||
let metadataContent = await Deno.readTextFile(metadataPath);
|
||||
|
||||
// Replace the envs line(s)
|
||||
if (metadataContent.includes("envs:")) {
|
||||
// Replace existing envs
|
||||
metadataContent = metadataContent.replace(
|
||||
/envs:\s*\n(\s+-\s+\S+\n?)*/,
|
||||
"envs:\n - NEW_VAR1\n - NEW_VAR2\n",
|
||||
);
|
||||
} else {
|
||||
// Add envs if not present
|
||||
metadataContent += "\nenvs:\n - NEW_VAR1\n - NEW_VAR2\n";
|
||||
}
|
||||
await Deno.writeTextFile(metadataPath, metadataContent);
|
||||
|
||||
// Push
|
||||
const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir);
|
||||
assertEquals(pushResult.code, 0, `Push failed: ${pushResult.stderr}`);
|
||||
|
||||
// Verify envs were updated on remote
|
||||
const getResp = await backend.apiRequest!(
|
||||
`/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`,
|
||||
);
|
||||
const scriptText = await getResp.text();
|
||||
assertEquals(getResp.ok, true, `Failed to get script: ${scriptText}`);
|
||||
const script = JSON.parse(scriptText);
|
||||
|
||||
assertEquals(
|
||||
script.envs,
|
||||
["NEW_VAR1", "NEW_VAR2"],
|
||||
`Script envs should be updated to new values. Got: ${JSON.stringify(script.envs)}`,
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "Integration: Script with empty envs is handled correctly",
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
async fn() {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
const uniqueId = Date.now();
|
||||
const scriptPath = `f/test/empty_envs_${uniqueId}`;
|
||||
|
||||
// Create folder
|
||||
await ensureDir(`${tempDir}/f/test`);
|
||||
await backend.apiRequest!(`/api/w/${backend.workspace}/folders/create`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: "test" }),
|
||||
});
|
||||
|
||||
// Create a script WITHOUT envs
|
||||
const createResp = await backend.apiRequest!(`/api/w/${backend.workspace}/scripts/create`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
path: scriptPath,
|
||||
content: `export async function main() {\n return "No envs";\n}`,
|
||||
summary: "Test no envs",
|
||||
description: "",
|
||||
language: "bun",
|
||||
kind: "script",
|
||||
}),
|
||||
});
|
||||
assertEquals(createResp.ok, true, `Failed to create script: ${await createResp.text()}`);
|
||||
|
||||
// Setup wmill.yaml
|
||||
await Deno.writeTextFile(
|
||||
`${tempDir}/wmill.yaml`,
|
||||
`defaultTs: bun
|
||||
includes:
|
||||
- "f/test/empty_envs_${uniqueId}**"
|
||||
excludes: []
|
||||
`,
|
||||
);
|
||||
|
||||
// Pull
|
||||
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
|
||||
assertEquals(pullResult.code, 0, `Pull failed: ${pullResult.stderr}`);
|
||||
|
||||
// Modify content
|
||||
const scriptFilePath = `${tempDir}/f/test/empty_envs_${uniqueId}.ts`;
|
||||
await Deno.writeTextFile(
|
||||
scriptFilePath,
|
||||
`export async function main() {\n return "Modified no envs";\n}`,
|
||||
);
|
||||
|
||||
// Push
|
||||
const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir);
|
||||
assertEquals(pushResult.code, 0, `Push failed: ${pushResult.stderr}`);
|
||||
|
||||
// Verify script was updated and envs is still null/empty
|
||||
const getResp = await backend.apiRequest!(
|
||||
`/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`,
|
||||
);
|
||||
const script = await getResp.json();
|
||||
|
||||
assert(
|
||||
script.content.includes("Modified no envs"),
|
||||
"Script content should be updated",
|
||||
);
|
||||
|
||||
// envs should be null, empty, or undefined
|
||||
assert(
|
||||
!script.envs || script.envs.length === 0,
|
||||
`Script envs should remain empty. Got: ${JSON.stringify(script.envs)}`,
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.623.0",
|
||||
"version": "1.621.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.623.0",
|
||||
"version": "1.621.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.623.0",
|
||||
"version": "1.621.2",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user