mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-11 00:06:06 +00:00
Merge branch 'main' into claude/issue-6173-20250713_231304
This commit is contained in:
@@ -88,6 +88,8 @@ jobs:
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git config pull.rebase true
|
||||
git pull origin ${{ env.BRANCH_NAME }}
|
||||
git add backend/.sqlx
|
||||
git commit -m "Update SQLx metadata"
|
||||
git push origin ${{ env.BRANCH_NAME }}
|
||||
@@ -158,6 +160,8 @@ jobs:
|
||||
git checkout $BRANCH_NAME
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git config pull.rebase true
|
||||
git pull origin $BRANCH_NAME
|
||||
git add backend/ee-repo-ref.txt
|
||||
git commit -m "Update ee-repo-ref.txt" || echo "No changes to commit"
|
||||
git push origin $BRANCH_NAME
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
# Changelog
|
||||
|
||||
## [1.507.0](https://github.com/windmill-labs/windmill/compare/v1.506.0...v1.507.0) (2025-07-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* git sync improvements ([#6182](https://github.com/windmill-labs/windmill/issues/6182)) ([aa37f64](https://github.com/windmill-labs/windmill/commit/aa37f643e78cae09be6246e0a2e84776b60afa49))
|
||||
* multi s3 arg input ([#6187](https://github.com/windmill-labs/windmill/issues/6187)) ([9053a93](https://github.com/windmill-labs/windmill/commit/9053a931ce4d075eddd8063de73ea95c3a346937))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** fix dev graph not loading ([#6190](https://github.com/windmill-labs/windmill/issues/6190)) ([885f711](https://github.com/windmill-labs/windmill/commit/885f711e03e4430d3fc4ab9756c94f0182b20bcc))
|
||||
* only close app dropdown when action is done ([cca8e74](https://github.com/windmill-labs/windmill/commit/cca8e748aa68d41da5f1f8196a02d72d8ca84547))
|
||||
* prevent worker not exiting if special case of same worker job ([2e1b6c1](https://github.com/windmill-labs/windmill/commit/2e1b6c1947e5e7f66061c5b380ce0483342df706))
|
||||
|
||||
## [1.506.0](https://github.com/windmill-labs/windmill/compare/v1.505.4...v1.506.0) (2025-07-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add oauth client_credentials support ([#6110](https://github.com/windmill-labs/windmill/issues/6110)) ([d562625](https://github.com/windmill-labs/windmill/commit/d562625474260d16699329884dd6c3b890f808ad))
|
||||
* **frontend:** app static and user resource picker default values ([#6179](https://github.com/windmill-labs/windmill/issues/6179)) ([b58c46a](https://github.com/windmill-labs/windmill/commit/b58c46a50452bce098695f52220946f87d62f840))
|
||||
|
||||
## [1.505.4](https://github.com/windmill-labs/windmill/compare/v1.505.3...v1.505.4) (2025-07-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* retry telemetry and renewal ([#6175](https://github.com/windmill-labs/windmill/issues/6175)) ([00ab0e8](https://github.com/windmill-labs/windmill/commit/00ab0e8f3883d2ef884438a957b8d26a4672836d))
|
||||
|
||||
## [1.505.3](https://github.com/windmill-labs/windmill/compare/v1.505.2...v1.505.3) (2025-07-14)
|
||||
|
||||
|
||||
|
||||
+5
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO account (workspace_id, client, expires_at, refresh_token) VALUES ($1, $2, now() + ($3 || ' seconds')::interval, $4) RETURNING id",
|
||||
"query": "INSERT INTO account (workspace_id, client, expires_at, refresh_token, grant_type, cc_client_id, cc_client_secret) VALUES ($1, $2, now() + ($3 || ' seconds')::interval, $4, $5, $6, $7) RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -14,6 +14,9 @@
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
@@ -21,5 +24,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "dc649111c4c862c36c26ad8a9eff8c31c2533a4a73184d0276f9a388ff2fa574"
|
||||
"hash": "00418f2e621cbe7aed536cff357898f7250544dfcb3bd85d314af1058f3148d8"
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+2
-2
@@ -18,8 +18,8 @@
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76"
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT client, refresh_token, grant_type, cc_client_id, cc_client_secret FROM account WHERE workspace_id = $1 AND id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "client",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "refresh_token",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "grant_type",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "cc_client_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "cc_client_secret",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b9d57959f2696f74e9d24f55fb78ff9e9041ab84b896a8b980df7b550ef27a22"
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE account SET refresh_token = $1, expires_at = $2 WHERE workspace_id = $3 AND id = $4",
|
||||
"query": "UPDATE account SET refresh_token = $1, expires_at = $2, refresh_error = NULL WHERE workspace_id = $3 AND id = $4",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -13,5 +13,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cac594031a21b4806de9c4616317d3541522ef9712a83ecff7bd8b5f6e870748"
|
||||
"hash": "db9831a3218b36c32b305b9f2ba21abceec74c9c277f27231dba12868e68126b"
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT client, refresh_token FROM account WHERE workspace_id = $1 AND id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "client",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "refresh_token",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "fbe14569717b4c4937b74e6dc6f6b4ea29a2b0c2b1cde48ef06d687f6f9e8f15"
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+5
-5
@@ -41,11 +41,11 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
Generated
+38
-36
@@ -5059,7 +5059,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"rustix 1.0.7",
|
||||
"rustix 1.0.8",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@@ -10493,7 +10493,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rmcp"
|
||||
version = "0.2.1"
|
||||
source = "git+https://github.com/modelcontextprotocol/rust-sdk?rev=c74b26b3e3c5dfd0be8e85dede8d88f33d46ff06#c74b26b3e3c5dfd0be8e85dede8d88f33d46ff06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37f2048a81a7ff7e8ef6bc5abced70c3d9114c8f03d85d7aaaafd9fd04f12e9e"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -10522,7 +10523,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rmcp-macros"
|
||||
version = "0.2.1"
|
||||
source = "git+https://github.com/modelcontextprotocol/rust-sdk?rev=c74b26b3e3c5dfd0be8e85dede8d88f33d46ff06#c74b26b3e3c5dfd0be8e85dede8d88f33d46ff06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72398e694b9f6dbb5de960cf158c8699e6a1854cb5bbaac7de0646b2005763c4"
|
||||
dependencies = [
|
||||
"darling 0.20.11",
|
||||
"proc-macro2",
|
||||
@@ -10719,15 +10721,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.0.7"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
|
||||
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.9.4",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -12904,7 +12906,7 @@ dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"rustix 1.0.7",
|
||||
"rustix 1.0.8",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@@ -12923,7 +12925,7 @@ version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
|
||||
dependencies = [
|
||||
"rustix 1.0.7",
|
||||
"rustix 1.0.8",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@@ -14674,7 +14676,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -14726,7 +14728,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -14837,7 +14839,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
@@ -14852,7 +14854,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
@@ -14865,7 +14867,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -14879,7 +14881,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -14957,7 +14959,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -14971,7 +14973,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -14994,7 +14996,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -15006,7 +15008,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -15015,7 +15017,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15027,7 +15029,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15039,7 +15041,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -15051,7 +15053,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15063,7 +15065,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15075,7 +15077,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -15086,7 +15088,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -15097,7 +15099,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -15109,7 +15111,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15132,7 +15134,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -15149,7 +15151,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15163,7 +15165,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15181,7 +15183,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wasm"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.16",
|
||||
@@ -15205,7 +15207,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15215,7 +15217,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15248,7 +15250,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
@@ -15258,7 +15260,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15900,7 +15902,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix 1.0.7",
|
||||
"rustix 1.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -32,7 +32,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
17b17f57e4c777e7e2ffc0e5def317f017a1683d
|
||||
3cccbe81494f7edd113dce2812278b00c91160c0
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Rollback client_credentials OAuth flow support
|
||||
ALTER TABLE account DROP COLUMN IF EXISTS grant_type;
|
||||
ALTER TABLE account DROP COLUMN IF EXISTS cc_client_id;
|
||||
ALTER TABLE account DROP COLUMN IF EXISTS cc_client_secret;
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Add support for client_credentials OAuth flow
|
||||
-- Add grant_type column to distinguish between OAuth flows for backwards compatibility
|
||||
ALTER TABLE account ADD COLUMN grant_type VARCHAR(50) NOT NULL DEFAULT 'authorization_code';
|
||||
|
||||
-- Add client_id and client_secret columns for resource-level client credentials
|
||||
-- These are used when grant_type = 'client_credentials' and user wants to use resource-level credentials
|
||||
-- instead of instance-level credentials configured by admin
|
||||
ALTER TABLE account
|
||||
ADD COLUMN cc_client_id VARCHAR(500),
|
||||
ADD COLUMN cc_client_secret VARCHAR(500);
|
||||
@@ -40,7 +40,7 @@ mcp = ["dep:rmcp"]
|
||||
python = []
|
||||
|
||||
[dependencies]
|
||||
rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk", rev = "c74b26b3e3c5dfd0be8e85dede8d88f33d46ff06", features=["transport-streamable-http-server", "transport-streamable-http-server-session", "transport-worker"], optional = true }
|
||||
rmcp = { version = "0.2.1", features=["transport-streamable-http-server", "transport-streamable-http-server-session", "transport-worker"], optional = true }
|
||||
windmill-queue.workspace = true
|
||||
windmill-common = { workspace = true, default-features = false }
|
||||
windmill-audit.workspace = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.505.3
|
||||
version: 1.507.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -581,7 +581,7 @@ paths:
|
||||
summary: get connected repositories
|
||||
operationId: getGlobalConnectedRepositories
|
||||
tags:
|
||||
- git_sync
|
||||
- Git Sync
|
||||
responses:
|
||||
"200":
|
||||
description: connected repositories
|
||||
@@ -3388,6 +3388,15 @@ paths:
|
||||
type: integer
|
||||
client:
|
||||
type: string
|
||||
grant_type:
|
||||
type: string
|
||||
default: "authorization_code"
|
||||
cc_client_id:
|
||||
type: string
|
||||
description: "OAuth client ID for resource-level credentials (client_credentials flow only)"
|
||||
cc_client_secret:
|
||||
type: string
|
||||
description: "OAuth client secret for resource-level credentials (client_credentials flow only)"
|
||||
required:
|
||||
- expires_in
|
||||
- client
|
||||
@@ -3399,6 +3408,48 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/oauth/connect_client_credentials/{client}:
|
||||
post:
|
||||
summary: connect OAuth using client credentials
|
||||
operationId: connectClientCredentials
|
||||
tags:
|
||||
- oauth
|
||||
parameters:
|
||||
- name: client
|
||||
in: path
|
||||
description: OAuth client name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
description: client credentials flow parameters
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
scopes:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
cc_client_id:
|
||||
type: string
|
||||
description: "OAuth client ID for resource-level authentication"
|
||||
cc_client_secret:
|
||||
type: string
|
||||
description: "OAuth client secret for resource-level authentication"
|
||||
required:
|
||||
- cc_client_id
|
||||
- cc_client_secret
|
||||
responses:
|
||||
"200":
|
||||
description: OAuth token response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TokenResponse"
|
||||
|
||||
/w/{workspace}/oauth/refresh_token/{id}:
|
||||
post:
|
||||
summary: refresh token
|
||||
@@ -3550,6 +3601,10 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
grant_types:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
/teams/sync:
|
||||
post:
|
||||
@@ -12283,8 +12338,7 @@ paths:
|
||||
"200":
|
||||
description: Parquet Preview
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
application/json: {}
|
||||
|
||||
/w/{workspace}/job_helpers/load_table_count/{path}:
|
||||
get:
|
||||
@@ -12363,8 +12417,7 @@ paths:
|
||||
"200":
|
||||
description: Csv Preview
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
application/json: {}
|
||||
|
||||
/w/{workspace}/job_helpers/delete_s3_file:
|
||||
delete:
|
||||
@@ -16858,6 +16911,8 @@ components:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
grant_type:
|
||||
type: string
|
||||
|
||||
required:
|
||||
- access_token
|
||||
@@ -16983,27 +17038,6 @@ components:
|
||||
WorkspaceGitSyncSettings:
|
||||
type: object
|
||||
properties:
|
||||
include_path:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
include_type:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- script
|
||||
- flow
|
||||
- app
|
||||
- folder
|
||||
- resource
|
||||
- variable
|
||||
- secret
|
||||
- resourcetype
|
||||
- schedule
|
||||
- user
|
||||
- group
|
||||
- trigger
|
||||
repositories:
|
||||
type: array
|
||||
items:
|
||||
@@ -17055,6 +17089,42 @@ components:
|
||||
type: boolean
|
||||
group_by_folder:
|
||||
type: boolean
|
||||
collapsed:
|
||||
type: boolean
|
||||
settings:
|
||||
type: object
|
||||
properties:
|
||||
include_path:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
include_type:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- script
|
||||
- flow
|
||||
- app
|
||||
- folder
|
||||
- resource
|
||||
- variable
|
||||
- secret
|
||||
- resourcetype
|
||||
- schedule
|
||||
- user
|
||||
- group
|
||||
- trigger
|
||||
- settings
|
||||
- key
|
||||
exclude_path:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
extra_include_path:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
exclude_types_override:
|
||||
type: array
|
||||
items:
|
||||
@@ -17072,6 +17142,8 @@ components:
|
||||
- user
|
||||
- group
|
||||
- trigger
|
||||
- settings
|
||||
- key
|
||||
required:
|
||||
- script_path
|
||||
- git_repo_resource_path
|
||||
|
||||
@@ -47,7 +47,7 @@ use windmill_common::{
|
||||
oauth2::WORKSPACE_SLACK_BOT_TOKEN_PATH,
|
||||
utils::{paginate, rd_string, require_admin, Pagination},
|
||||
};
|
||||
use windmill_git_sync::handle_deployment_metadata;
|
||||
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::utils::require_admin_or_devops;
|
||||
@@ -636,6 +636,17 @@ async fn edit_deploy_to(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Settings { setting_type: "deploy_to".to_string() },
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("Edit deploy to for {}", &w_id))
|
||||
}
|
||||
|
||||
@@ -701,6 +712,17 @@ async fn edit_webhook(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Settings { setting_type: "webhook".to_string() },
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("Edit webhook for workspace {}", &w_id))
|
||||
}
|
||||
|
||||
@@ -741,6 +763,18 @@ async fn edit_copilot_config(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
// Trigger git sync for AI config changes
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::Settings { setting_type: "ai_config".to_string() },
|
||||
Some("AI configuration updated".to_string()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("Edit copilot config for workspace {}", &w_id))
|
||||
}
|
||||
|
||||
@@ -818,6 +852,20 @@ async fn edit_large_file_storage_config(
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
// Trigger git sync for large file storage changes
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::Settings {
|
||||
setting_type: "large_file_storage".to_string(),
|
||||
},
|
||||
Some("Large file storage configuration updated".to_string()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!(
|
||||
"Edit large file storage config for workspace {}",
|
||||
&w_id
|
||||
@@ -887,6 +935,18 @@ async fn edit_git_sync_config(
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
// Trigger git sync for git sync settings changes
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::Settings { setting_type: "git_sync".to_string() },
|
||||
Some("Git sync configuration updated".to_string()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("Edit git sync config for workspace {}", &w_id))
|
||||
}
|
||||
|
||||
@@ -1014,6 +1074,18 @@ async fn edit_default_scripts(
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
// Trigger git sync for default scripts changes
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::Settings { setting_type: "default_scripts".to_string() },
|
||||
Some("Default scripts configuration updated".to_string()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("Edit default scripts for workspace {}", &w_id))
|
||||
}
|
||||
|
||||
@@ -1084,6 +1156,18 @@ async fn edit_default_app(
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
// Trigger git sync for default app changes
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::Settings { setting_type: "default_app".to_string() },
|
||||
Some("Default app configuration updated".to_string()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("Edit default app for workspace {}", &w_id))
|
||||
}
|
||||
|
||||
@@ -1160,6 +1244,18 @@ async fn edit_error_handler(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
// Trigger git sync for error handler changes
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::Settings { setting_type: "error_handler".to_string() },
|
||||
Some("Error handler configuration updated".to_string()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("Edit error_handler for workspace {}", &w_id))
|
||||
}
|
||||
|
||||
@@ -1201,6 +1297,7 @@ async fn set_environment_variable(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!("Set environment variable {}", name))
|
||||
}
|
||||
None => {
|
||||
@@ -1223,6 +1320,7 @@ async fn set_environment_variable(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!("Deleted environment variable {}", name))
|
||||
}
|
||||
}
|
||||
@@ -1316,6 +1414,18 @@ async fn set_encryption_key(
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger git sync for encryption key changes
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::Key { key_type: "encryption_key".to_string() },
|
||||
Some("Encryption key updated".to_string()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -2053,6 +2163,18 @@ async fn change_workspace_name(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
// Trigger git sync for workspace name changes
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::Settings { setting_type: "workspace_name".to_string() },
|
||||
Some(format!("Workspace name updated to {}", &rw.new_name)),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("updated workspace name to {}", &rw.new_name))
|
||||
}
|
||||
|
||||
@@ -2076,6 +2198,17 @@ async fn change_workspace_color(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Settings { setting_type: "workspace_color".to_string() },
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!(
|
||||
"updated workspace color to {}",
|
||||
rw.color.as_deref().unwrap_or("no color")
|
||||
@@ -2154,10 +2287,10 @@ pub struct MuteCriticalAlertRequest {
|
||||
async fn mute_critical_alerts(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
ApiAuthed { is_admin, username, .. }: ApiAuthed,
|
||||
authed: ApiAuthed,
|
||||
Json(m_r): Json<MuteCriticalAlertRequest>,
|
||||
) -> Result<String> {
|
||||
require_admin(is_admin, &username)?;
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
let mute_alerts = m_r.mute_critical_alerts.unwrap_or(false);
|
||||
|
||||
@@ -2178,6 +2311,17 @@ async fn mute_critical_alerts(
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Settings { setting_type: "critical_alerts".to_string() },
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!(
|
||||
"Updated mute criticital alert ui settings for workspace: {}",
|
||||
&w_id
|
||||
@@ -2225,5 +2369,19 @@ async fn update_operator_settings(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
// Trigger git sync for operator settings changes
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::Settings {
|
||||
setting_type: "operator_settings".to_string(),
|
||||
},
|
||||
Some("Operator settings updated".to_string()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok("Operator settings updated successfully".to_string())
|
||||
}
|
||||
|
||||
@@ -3,9 +3,15 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||
pub struct WorkspaceGitSyncSettings {
|
||||
pub include_path: Vec<String>,
|
||||
pub include_type: Vec<ObjectType>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub include_path: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub include_type: Option<Vec<ObjectType>>,
|
||||
pub repositories: Vec<GitRepositorySettings>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub exclude_path: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_include_path: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||
@@ -14,7 +20,7 @@ pub struct WorkspaceDeploymentUISettings {
|
||||
pub include_type: Vec<ObjectType>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||
#[serde(rename_all(serialize = "lowercase", deserialize = "lowercase"))]
|
||||
pub enum ObjectType {
|
||||
Script,
|
||||
@@ -29,15 +35,41 @@ pub enum ObjectType {
|
||||
User,
|
||||
Group,
|
||||
Trigger,
|
||||
Settings,
|
||||
Key,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GitRepositorySettings {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub exclude_types_override: Option<Vec<ObjectType>>,
|
||||
pub script_path: String,
|
||||
pub git_repo_resource_path: String,
|
||||
pub use_individual_branch: Option<bool>,
|
||||
pub group_by_folder: Option<bool>,
|
||||
pub exclude_types_override: Option<Vec<ObjectType>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub settings: Option<GitSyncSettings>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GitSyncSettings {
|
||||
pub include_path: Vec<String>,
|
||||
pub include_type: Vec<ObjectType>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub exclude_path: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_include_path: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Default for GitSyncSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
include_path: Vec::new(),
|
||||
include_type: Vec::new(),
|
||||
exclude_path: None,
|
||||
extra_include_path: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
|
||||
@@ -37,6 +37,8 @@ pub enum DeployedObject {
|
||||
MqttTrigger { path: String },
|
||||
SqsTrigger { path: String },
|
||||
GcpTrigger { path: String },
|
||||
Settings { setting_type: String },
|
||||
Key { key_type: String },
|
||||
}
|
||||
|
||||
impl DeployedObject {
|
||||
@@ -60,12 +62,14 @@ impl DeployedObject {
|
||||
DeployedObject::MqttTrigger { path } => path.to_owned(),
|
||||
DeployedObject::SqsTrigger { path } => path.to_owned(),
|
||||
DeployedObject::GcpTrigger { path } => path.to_owned(),
|
||||
DeployedObject::Settings { .. } => "settings.yaml".to_string(),
|
||||
DeployedObject::Key { .. } => "encryption_key.yaml".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ignore_regex_filter(&self) -> bool {
|
||||
match self {
|
||||
Self::User { .. } | Self::Group { .. } | Self::ResourceType { .. } => true,
|
||||
Self::User { .. } | Self::Group { .. } | Self::ResourceType { .. } | Self::Settings { .. } | Self::Key { .. } => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -90,6 +94,8 @@ impl DeployedObject {
|
||||
DeployedObject::MqttTrigger { .. } => None,
|
||||
DeployedObject::SqsTrigger { .. } => None,
|
||||
DeployedObject::GcpTrigger { .. } => None,
|
||||
DeployedObject::Settings { .. } => None,
|
||||
DeployedObject::Key { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2310,7 +2310,17 @@ pub async fn pull(
|
||||
query_o: Option<&(String, String)>,
|
||||
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
|
||||
) -> windmill_common::error::Result<PulledJobResult> {
|
||||
let mut pull_loop_count = 0;
|
||||
loop {
|
||||
pull_loop_count += 1;
|
||||
if pull_loop_count % 10 == 0 {
|
||||
tracing::warn!("Pull job loop count: {}", pull_loop_count);
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
if pull_loop_count > 1000 {
|
||||
tracing::error!("Pull job loop count exceeded 1000, breaking");
|
||||
return Ok(PulledJobResult { job: None, suspended: false });
|
||||
}
|
||||
if let Some((query_suspended, query_no_suspend)) = query_o {
|
||||
let njob = {
|
||||
let job = if query_suspended.is_empty() {
|
||||
@@ -2513,6 +2523,14 @@ pub async fn pull(
|
||||
i += 1;
|
||||
estimated_next_schedule_timestamp = estimated_next_schedule_timestamp + inc;
|
||||
}
|
||||
if i % 50 == 0 {
|
||||
tracing::warn!("Window finding for job {} loop count: {}", job_uuid, pull_loop_count);
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
if i > 1000000000 {
|
||||
tracing::error!("Window finding job loop count exceeded 1000000000, breaking");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("Job '{}' from path '{}' with concurrency key '{}' has reached its concurrency limit of {} jobs run in the last {} seconds. This job will be re-queued for next execution at {} (min_started_at: {min_started_at_or_now}, avg script duration: {:?}, number of time windows full: {})",
|
||||
|
||||
@@ -29,7 +29,7 @@ use windmill_queue::{
|
||||
|
||||
use serde_json::{json, value::RawValue, Value};
|
||||
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::{sync::Notify, task::JoinHandle};
|
||||
|
||||
use windmill_queue::{add_completed_job, add_completed_job_error};
|
||||
|
||||
@@ -112,6 +112,7 @@ async fn process_jc(
|
||||
enum JobCompletedRx {
|
||||
JobCompleted(SendResult),
|
||||
Killpill,
|
||||
WakeUp,
|
||||
}
|
||||
|
||||
pub fn start_background_processor(
|
||||
@@ -119,6 +120,7 @@ pub fn start_background_processor(
|
||||
job_completed_sender: JobCompletedSender,
|
||||
same_worker_queue_size: Arc<AtomicU16>,
|
||||
job_completed_processor_is_done: Arc<AtomicBool>,
|
||||
wake_up_notify: Arc<Notify>,
|
||||
last_processing_duration: Arc<AtomicU16>,
|
||||
base_internal_url: String,
|
||||
db: DB,
|
||||
@@ -155,7 +157,11 @@ pub fn start_background_processor(
|
||||
}
|
||||
result = bounded_rx.recv_async() => {
|
||||
result.ok().map(JobCompletedRx::JobCompleted)
|
||||
}
|
||||
},
|
||||
_ = wake_up_notify.notified() => {
|
||||
tracing::info!("bg processor received wake up signal, checking if same worker queue is empty");
|
||||
Some(JobCompletedRx::WakeUp)
|
||||
},
|
||||
_ = killpill_rx.recv() => {
|
||||
tracing::info!("bg processor received killpill signal, queuing killpill job");
|
||||
Some(JobCompletedRx::Killpill)
|
||||
@@ -254,6 +260,8 @@ pub fn start_background_processor(
|
||||
JobCompletedRx::Killpill => {
|
||||
tracing::info!("killpill job received, processing only same worker jobs");
|
||||
has_been_killed = true;
|
||||
},
|
||||
JobCompletedRx::WakeUp => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1198,12 +1198,15 @@ pub async fn run_worker(
|
||||
let job_completed_processor_is_done =
|
||||
Arc::new(AtomicBool::new(matches!(conn, Connection::Http(_))));
|
||||
|
||||
// This is used to wake up the background processor when main loop is done and just waiting for new same workers jobs, and that bg processor is also not processing any jobs, bg processing can exit if no more same worker jobs
|
||||
let wake_up_notify = Arc::new(tokio::sync::Notify::new());
|
||||
let send_result = match (conn, job_completed_rx) {
|
||||
(Connection::Sql(db), Some(job_completed_receiver)) => Some(start_background_processor(
|
||||
job_completed_receiver,
|
||||
job_completed_tx.clone(),
|
||||
same_worker_queue_size.clone(),
|
||||
job_completed_processor_is_done.clone(),
|
||||
wake_up_notify.clone(),
|
||||
last_processing_duration.clone(),
|
||||
base_internal_url.to_string(),
|
||||
db.clone(),
|
||||
@@ -1489,6 +1492,7 @@ pub async fn run_worker(
|
||||
tracing::info!(worker = %worker_name, hostname = %hostname, "all running jobs have completed and all completed jobs have been fully processed, exiting");
|
||||
break;
|
||||
} else {
|
||||
wake_up_notify.notify_one();
|
||||
tracing::info!(worker = %worker_name, hostname = %hostname, "there may be same_worker jobs to process later, waiting for job_completed_processor to finish progressing all remaining flows before exiting");
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
continue;
|
||||
|
||||
+1
-1
@@ -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.505.3";
|
||||
export const VERSION = "v1.507.0";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Testing Guide for Windmill CLI
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
deno test -A --no-check test/
|
||||
|
||||
# Run specific test files
|
||||
deno test -A --no-check test/gitsync_settings_features.test.ts
|
||||
deno test -A --no-check test/init_no_git_sync.test.ts
|
||||
deno test -A --no-check test/multi_instance_workspace.test.ts
|
||||
deno test -A --no-check test/override_settings_behavior.test.ts
|
||||
deno test -A --no-check test/sync_config_resolution.test.ts
|
||||
deno test -A --no-check test/workspace_conflicts.test.ts
|
||||
|
||||
# Run with specific test patterns
|
||||
deno test -A --no-check test/ --filter "workspace"
|
||||
deno test -A --no-check test/ --filter "sync"
|
||||
```
|
||||
|
||||
## Test Files
|
||||
|
||||
- **`gitsync_settings_features.test.ts`** - Git sync settings functionality
|
||||
- **`init_no_git_sync.test.ts`** - Init without git sync
|
||||
- **`multi_instance_workspace.test.ts`** - Multi-instance workspace handling
|
||||
- **`override_settings_behavior.test.ts`** - Settings override behavior
|
||||
- **`sync_config_resolution.test.ts`** - Sync configuration resolution
|
||||
- **`workspace_conflicts.test.ts`** - Workspace conflict detection
|
||||
|
||||
## Docker Requirements
|
||||
|
||||
```bash
|
||||
# Ensure Docker is running
|
||||
docker --version
|
||||
docker-compose --version
|
||||
|
||||
# Ensure EE license key is available
|
||||
echo $EE_LICENSE_KEY
|
||||
```
|
||||
|
||||
## Debugging Failed Tests
|
||||
|
||||
```bash
|
||||
# Run with verbose output
|
||||
deno test -A --no-check test/ --reporter=verbose
|
||||
|
||||
# Check container status
|
||||
docker ps
|
||||
|
||||
# View backend logs
|
||||
docker logs test-test_windmill_server-1
|
||||
|
||||
# Manual container management
|
||||
cd test
|
||||
docker compose -f docker-compose.test.yml up -d
|
||||
docker compose -f docker-compose.test.yml down
|
||||
docker compose -f docker-compose.test.yml down -v
|
||||
```
|
||||
+71
@@ -13,6 +13,10 @@ export interface SyncOptions {
|
||||
skipResources?: boolean;
|
||||
skipResourceTypes?: boolean;
|
||||
skipSecrets?: boolean;
|
||||
skipScripts?: boolean;
|
||||
skipFlows?: boolean;
|
||||
skipApps?: boolean;
|
||||
skipFolders?: boolean;
|
||||
includeSchedules?: boolean;
|
||||
includeTriggers?: boolean;
|
||||
includeUsers?: boolean;
|
||||
@@ -26,6 +30,8 @@ export interface SyncOptions {
|
||||
defaultTs?: "bun" | "deno";
|
||||
codebases?: Codebase[];
|
||||
parallel?: number;
|
||||
jsonOutput?: boolean;
|
||||
overrides?: { [key: string]: Partial<SyncOptions> };
|
||||
}
|
||||
|
||||
export interface Codebase {
|
||||
@@ -59,9 +65,74 @@ export async function readConfigFile(): Promise<SyncOptions> {
|
||||
}
|
||||
}
|
||||
|
||||
// Default sync options - shared across the codebase to prevent duplication
|
||||
export const DEFAULT_SYNC_OPTIONS: Readonly<Required<Pick<SyncOptions,
|
||||
'defaultTs' | 'includes' | 'excludes' | 'codebases' | 'skipVariables' | 'skipResources' |
|
||||
'skipResourceTypes' | 'skipSecrets' | 'includeSchedules' | 'includeTriggers' |
|
||||
'skipScripts' | 'skipFlows' | 'skipApps' | 'skipFolders' |
|
||||
'includeUsers' | 'includeGroups' | 'includeSettings' | 'includeKey'
|
||||
>>> = {
|
||||
defaultTs: 'bun',
|
||||
includes: ['f/**'],
|
||||
excludes: [],
|
||||
codebases: [],
|
||||
skipVariables: false,
|
||||
skipResources: false,
|
||||
skipResourceTypes: false,
|
||||
skipSecrets: true,
|
||||
skipScripts: false,
|
||||
skipFlows: false,
|
||||
skipApps: false,
|
||||
skipFolders: false,
|
||||
includeSchedules: false,
|
||||
includeTriggers: false,
|
||||
includeUsers: false,
|
||||
includeGroups: false,
|
||||
includeSettings: false,
|
||||
includeKey: false
|
||||
} as const;
|
||||
|
||||
export async function mergeConfigWithConfigFile<T>(
|
||||
opts: T
|
||||
): Promise<T & SyncOptions> {
|
||||
const configFile = await readConfigFile();
|
||||
return Object.assign(configFile ?? {}, opts);
|
||||
}
|
||||
|
||||
// Get effective settings by merging top-level settings and overrides
|
||||
export function getEffectiveSettings(
|
||||
config: SyncOptions,
|
||||
baseUrl: string,
|
||||
workspaceId: string,
|
||||
repo: string
|
||||
): SyncOptions {
|
||||
// Start with empty object - no defaults
|
||||
let effective = {} as SyncOptions;
|
||||
|
||||
// Merge top-level settings from config (which contains user's chosen defaults)
|
||||
Object.keys(config).forEach(key => {
|
||||
if (key !== 'overrides' && config[key as keyof SyncOptions] !== undefined) {
|
||||
(effective as any)[key] = config[key as keyof SyncOptions];
|
||||
}
|
||||
});
|
||||
|
||||
if (!config.overrides) {
|
||||
return effective;
|
||||
}
|
||||
|
||||
// Construct override keys using the single format
|
||||
const workspaceKey = `${baseUrl}:${workspaceId}:*`;
|
||||
const repoKey = `${baseUrl}:${workspaceId}:${repo}`;
|
||||
|
||||
// Apply workspace-level overrides
|
||||
if (config.overrides[workspaceKey]) {
|
||||
Object.assign(effective, config.overrides[workspaceKey]);
|
||||
}
|
||||
|
||||
// Apply repository-specific overrides (overrides workspace-level)
|
||||
if (config.overrides[repoKey]) {
|
||||
Object.assign(effective, config.overrides[repoKey]);
|
||||
}
|
||||
|
||||
return effective;
|
||||
}
|
||||
|
||||
+58
-5
@@ -30,7 +30,7 @@ async function tryResolveWorkspace(
|
||||
if (cache) return { isError: false, value: cache };
|
||||
|
||||
if (opts.workspace) {
|
||||
const e = await getWorkspaceByName(opts.workspace);
|
||||
const e = await getWorkspaceByName(opts.workspace, opts.configDir);
|
||||
if (!e) {
|
||||
return {
|
||||
isError: true,
|
||||
@@ -57,8 +57,48 @@ export async function resolveWorkspace(
|
||||
): Promise<Workspace> {
|
||||
if (opts.baseUrl) {
|
||||
if (opts.workspace && opts.token) {
|
||||
const normalizedBaseUrl = new URL(opts.baseUrl).toString(); // add trailing slash if not present
|
||||
|
||||
// Try to find existing workspace profile by name, then by workspaceId + remote
|
||||
if (opts.workspace) {
|
||||
// Try by workspace name first
|
||||
let existingWorkspace = await getWorkspaceByName(opts.workspace, opts.configDir);
|
||||
|
||||
// If not found by name, try to find by workspaceId + remote match
|
||||
if (!existingWorkspace) {
|
||||
const { allWorkspaces } = await import("./workspace.ts");
|
||||
const workspaces = await allWorkspaces(opts.configDir);
|
||||
const matchingWorkspaces = workspaces.filter(
|
||||
w => w.workspaceId === opts.workspace && w.remote === normalizedBaseUrl
|
||||
);
|
||||
|
||||
// Due to uniqueness constraint, there can only be 0 or 1 match
|
||||
if (matchingWorkspaces.length === 1) {
|
||||
existingWorkspace = matchingWorkspaces[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (existingWorkspace) {
|
||||
// Validate that the base URL matches the profile's remote
|
||||
if (existingWorkspace.remote !== normalizedBaseUrl) {
|
||||
log.info(
|
||||
colors.red(
|
||||
`Base URL mismatch: --base-url is ${normalizedBaseUrl} but workspace profile "${opts.workspace}" uses ${existingWorkspace.remote}`
|
||||
)
|
||||
);
|
||||
return Deno.exit(-1);
|
||||
}
|
||||
// Use the existing workspace profile (preserves workspace name)
|
||||
return {
|
||||
...existingWorkspace,
|
||||
token: opts.token, // Use the provided token
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// No existing profile found, create temporary workspace
|
||||
return {
|
||||
remote: new URL(opts.baseUrl).toString(), // add trailing slash if not present
|
||||
remote: normalizedBaseUrl,
|
||||
workspaceId: opts.workspace,
|
||||
name: opts.workspace,
|
||||
token: opts.token,
|
||||
@@ -95,20 +135,26 @@ export async function requireLogin(
|
||||
|
||||
try {
|
||||
return await wmill.globalWhoami();
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Check for network errors and provide clearer messages
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
if (errorMsg.includes('fetch') || errorMsg.includes('connection') || errorMsg.includes('ECONNREFUSED') || errorMsg.includes('refused')) {
|
||||
throw new Error(`Network error: Could not connect to Windmill server at ${workspace.remote}`);
|
||||
}
|
||||
|
||||
log.info(
|
||||
"! Could not reach API given existing credentials. Attempting to reauth..."
|
||||
);
|
||||
const newToken = await loginInteractive(workspace.remote);
|
||||
if (!newToken) {
|
||||
throw new Error("Could not reauth");
|
||||
throw new Error("Unauthorized: Could not authenticate with the provided credentials");
|
||||
}
|
||||
removeWorkspace(workspace.name, false, opts);
|
||||
workspace.token = newToken;
|
||||
addWorkspace(workspace, opts);
|
||||
|
||||
setClient(
|
||||
token,
|
||||
newToken,
|
||||
workspace.remote.substring(0, workspace.remote.length - 1)
|
||||
);
|
||||
return await wmill.globalWhoami();
|
||||
@@ -130,6 +176,13 @@ export async function fetchVersion(baseUrl: string): Promise<string> {
|
||||
new URL(new URL(baseUrl).origin + "/api/version"),
|
||||
{ headers: requestHeaders, method: "GET" }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
// Consume response body even on error to avoid resource leak
|
||||
await response.text();
|
||||
throw new Error(`Failed to fetch version: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.text();
|
||||
}
|
||||
export async function tryResolveVersion(
|
||||
|
||||
Generated
+252
@@ -2184,6 +2184,258 @@
|
||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
|
||||
}
|
||||
},
|
||||
"remote": {
|
||||
"https://deno.land/std@0.207.0/yaml/_dumper/dumper.ts": "717403d0e700de783f2ef5c906b3d7245383e1509fc050e7ff5d4a53a03dbf40",
|
||||
"https://deno.land/std@0.207.0/yaml/_dumper/dumper_state.ts": "f0d0673ceea288334061ca34b63954c2bb5feb5bf6de5e4cfe9a942cdf6e5efe",
|
||||
"https://deno.land/std@0.207.0/yaml/_error.ts": "b59e2c76ce5a47b1b9fa0ff9f96c1dd92ea1e1b17ce4347ece5944a95c3c1a84",
|
||||
"https://deno.land/std@0.207.0/yaml/_loader/loader.ts": "63ec7f0a265dbbabc54b25a4beefff7650e205160a2d75c7d8f8363b5f84851a",
|
||||
"https://deno.land/std@0.207.0/yaml/_loader/loader_state.ts": "0841870b467169269d7c2dfa75cd288c319bc06f65edd9e42c29e5fced91c7a4",
|
||||
"https://deno.land/std@0.207.0/yaml/_mark.ts": "dcd8585dee585e024475e9f3fe27d29740670fb64ebb970388094cad0fc11d5d",
|
||||
"https://deno.land/std@0.207.0/yaml/_state.ts": "ef03d55ec235d48dcfbecc0ab3ade90bfae69a61094846e08003421c2cf5cfc6",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/binary.ts": "24d49614463a7339a8a16d894919c2ec18a10588ae360ec352093b60e2cc8b0d",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/bool.ts": "5bfa75da84343d45347b521ba4e5aeace9fe6f53447405290d53315a3fc20e66",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/float.ts": "056bd3cb9c5586238b20517511014fb24b0e36f98f9f6073e12da308b6b9808a",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/function.ts": "ff574fe84a750695302864e1c31b93f12d14ada4bde79a5f93197fc33ad17471",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/int.ts": "563ad074f0fa7aecf6b6c3d84135bcc95a8269dcc15de878de20ce868fd773fa",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/map.ts": "7b105e4ab03a361c61e7e335a0baf4d40f06460b13920e5af3fb2783a1464000",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/merge.ts": "8192bf3e4d637f32567917f48bb276043da9cf729cf594e5ec191f7cd229337e",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/mod.ts": "060e2b3d38725094b77ea3a3f05fc7e671fced8e67ca18e525be98c4aa8f4bbb",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/nil.ts": "606e8f0c44d73117c81abec822f89ef81e40f712258c74f186baa1af659b8887",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/omap.ts": "cfe59a294726f5cea705c39a61fd2b08199cf48f4ccd6b040cb550ec0f38d0a1",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/pairs.ts": "0032fdfe57558d21696a4f8cf5b5cfd1f698743177080affc18629685c905666",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/regexp.ts": "1ce118de15b2da43b4bd8e4395f42d448b731acf3bdaf7c888f40789f9a95f8b",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/seq.ts": "95333abeec8a7e4d967b8c8328b269e342a4bbdd2585395549b9c4f58c8533a2",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/set.ts": "f28ba44e632ef2a6eb580486fd47a460445eeddbdf1dbc739c3e62486f566092",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/str.ts": "a67a3c6e429d95041399e964015511779b1130ea5889fa257c48457bd3446e31",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/timestamp.ts": "706ea80a76a73e48efaeb400ace087da1f927647b53ad6f754f4e06d51af087f",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/undefined.ts": "94a316ca450597ccbc6750cbd79097ad0d5f3a019797eed3c841a040c29540ba",
|
||||
"https://deno.land/std@0.207.0/yaml/_utils.ts": "26b311f0d42a7ce025060bd6320a68b50e52fd24a839581eb31734cd48e20393",
|
||||
"https://deno.land/std@0.207.0/yaml/mod.ts": "28ecda6652f3e7a7735ee29c247bfbd32a2e2fc5724068e9fd173ec4e59f66f7",
|
||||
"https://deno.land/std@0.207.0/yaml/parse.ts": "1fbbda572bf3fff578b6482c0d8b85097a38de3176bf3ab2ca70c25fb0c960ef",
|
||||
"https://deno.land/std@0.207.0/yaml/schema.ts": "96908b78dc50c340074b93fc1598d5e7e2fe59103f89ff81e5a49b2dedf77a67",
|
||||
"https://deno.land/std@0.207.0/yaml/schema/core.ts": "fa406f18ceedc87a50e28bb90ec7a4c09eebb337f94ef17468349794fa828639",
|
||||
"https://deno.land/std@0.207.0/yaml/schema/default.ts": "0047e80ae8a4a93293bc4c557ae8a546aabd46bb7165b9d9b940d57b4d88bde9",
|
||||
"https://deno.land/std@0.207.0/yaml/schema/extended.ts": "0784416bf062d20a1626b53c03380e265b3e39b9409afb9f4cb7d659fd71e60d",
|
||||
"https://deno.land/std@0.207.0/yaml/schema/failsafe.ts": "d219ab5febc43f770917d8ec37735a4b1ad671149846cbdcade767832b42b92b",
|
||||
"https://deno.land/std@0.207.0/yaml/schema/json.ts": "5f41dd7c2f1ad545ef6238633ce9ee3d444dfc5a18101e1768bd5504bf90e5e5",
|
||||
"https://deno.land/std@0.207.0/yaml/schema/mod.ts": "4472e827bab5025e92bc2eb2eeefa70ecbefc64b2799b765c69af84822efef32",
|
||||
"https://deno.land/std@0.207.0/yaml/stringify.ts": "fffc09c65c68d3d63f8159e8cbaa3f489bc20a8e55b4fbb61a8c2e9f914d1d02",
|
||||
"https://deno.land/std@0.207.0/yaml/type.ts": "65553da3da3c029b6589c6e4903f0afbea6768be8fca61580711457151f2b30f",
|
||||
"https://deno.land/std@0.208.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9",
|
||||
"https://deno.land/std@0.208.0/assert/_diff.ts": "58e1461cc61d8eb1eacbf2a010932bf6a05b79344b02ca38095f9b805795dc48",
|
||||
"https://deno.land/std@0.208.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7",
|
||||
"https://deno.land/std@0.208.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee",
|
||||
"https://deno.land/std@0.208.0/assert/assert_almost_equals.ts": "e15ca1f34d0d5e0afae63b3f5d975cbd18335a132e42b0c747d282f62ad2cd6c",
|
||||
"https://deno.land/std@0.208.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9",
|
||||
"https://deno.land/std@0.208.0/assert/assert_equals.ts": "d8ec8a22447fbaf2fc9d7c3ed2e66790fdb74beae3e482855d75782218d68227",
|
||||
"https://deno.land/std@0.208.0/assert/assert_exists.ts": "407cb6b9fb23a835cd8d5ad804e2e2edbbbf3870e322d53f79e1c7a512e2efd7",
|
||||
"https://deno.land/std@0.208.0/assert/assert_false.ts": "0ccbcaae910f52c857192ff16ea08bda40fdc79de80846c206bfc061e8c851c6",
|
||||
"https://deno.land/std@0.208.0/assert/assert_greater.ts": "ae2158a2d19313bf675bf7251d31c6dc52973edb12ac64ac8fc7064152af3e63",
|
||||
"https://deno.land/std@0.208.0/assert/assert_greater_or_equal.ts": "1439da5ebbe20855446cac50097ac78b9742abe8e9a43e7de1ce1426d556e89c",
|
||||
"https://deno.land/std@0.208.0/assert/assert_instance_of.ts": "3aedb3d8186e120812d2b3a5dea66a6e42bf8c57a8bd927645770bd21eea554c",
|
||||
"https://deno.land/std@0.208.0/assert/assert_is_error.ts": "c21113094a51a296ffaf036767d616a78a2ae5f9f7bbd464cd0197476498b94b",
|
||||
"https://deno.land/std@0.208.0/assert/assert_less.ts": "aec695db57db42ec3e2b62e97e1e93db0063f5a6ec133326cc290ff4b71b47e4",
|
||||
"https://deno.land/std@0.208.0/assert/assert_less_or_equal.ts": "5fa8b6a3ffa20fd0a05032fe7257bf985d207b85685fdbcd23651b70f928c848",
|
||||
"https://deno.land/std@0.208.0/assert/assert_match.ts": "c4083f80600bc190309903c95e397a7c9257ff8b5ae5c7ef91e834704e672e9b",
|
||||
"https://deno.land/std@0.208.0/assert/assert_not_equals.ts": "9f1acab95bd1f5fc9a1b17b8027d894509a745d91bac1718fdab51dc76831754",
|
||||
"https://deno.land/std@0.208.0/assert/assert_not_instance_of.ts": "0c14d3dfd9ab7a5276ed8ed0b18c703d79a3d106102077ec437bfe7ed912bd22",
|
||||
"https://deno.land/std@0.208.0/assert/assert_not_match.ts": "3796a5b0c57a1ce6c1c57883dd4286be13a26f715ea662318ab43a8491a13ab0",
|
||||
"https://deno.land/std@0.208.0/assert/assert_not_strict_equals.ts": "4cdef83df17488df555c8aac1f7f5ec2b84ad161b6d0645ccdbcc17654e80c99",
|
||||
"https://deno.land/std@0.208.0/assert/assert_object_match.ts": "d8fc2867cfd92eeacf9cea621e10336b666de1874a6767b5ec48988838370b54",
|
||||
"https://deno.land/std@0.208.0/assert/assert_rejects.ts": "45c59724de2701e3b1f67c391d6c71c392363635aad3f68a1b3408f9efca0057",
|
||||
"https://deno.land/std@0.208.0/assert/assert_strict_equals.ts": "b1f538a7ea5f8348aeca261d4f9ca603127c665e0f2bbfeb91fa272787c87265",
|
||||
"https://deno.land/std@0.208.0/assert/assert_string_includes.ts": "b821d39ebf5cb0200a348863c86d8c4c4b398e02012ce74ad15666fc4b631b0c",
|
||||
"https://deno.land/std@0.208.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd",
|
||||
"https://deno.land/std@0.208.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56",
|
||||
"https://deno.land/std@0.208.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece",
|
||||
"https://deno.land/std@0.208.0/assert/fail.ts": "c36353d7ae6e1f7933d45f8ea51e358c8c4b67d7e7502028598fe1fea062e278",
|
||||
"https://deno.land/std@0.208.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085",
|
||||
"https://deno.land/std@0.208.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a",
|
||||
"https://deno.land/std@0.208.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536",
|
||||
"https://deno.land/std@0.208.0/fmt/colors.ts": "34b3f77432925eb72cf0bfb351616949746768620b8e5ead66da532f93d10ba2",
|
||||
"https://deno.land/std@0.208.0/path/_common/assert_path.ts": "061e4d093d4ba5aebceb2c4da3318bfe3289e868570e9d3a8e327d91c2958946",
|
||||
"https://deno.land/std@0.208.0/path/_common/basename.ts": "0d978ff818f339cd3b1d09dc914881f4d15617432ae519c1b8fdc09ff8d3789a",
|
||||
"https://deno.land/std@0.208.0/path/_common/common.ts": "9e4233b2eeb50f8b2ae10ecc2108f58583aea6fd3e8907827020282dc2b76143",
|
||||
"https://deno.land/std@0.208.0/path/_common/constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0",
|
||||
"https://deno.land/std@0.208.0/path/_common/dirname.ts": "2ba7fb4cc9fafb0f38028f434179579ce61d4d9e51296fad22b701c3d3cd7397",
|
||||
"https://deno.land/std@0.208.0/path/_common/format.ts": "11aa62e316dfbf22c126917f5e03ea5fe2ee707386555a8f513d27ad5756cf96",
|
||||
"https://deno.land/std@0.208.0/path/_common/from_file_url.ts": "ef1bf3197d2efbf0297a2bdbf3a61d804b18f2bcce45548ae112313ec5be3c22",
|
||||
"https://deno.land/std@0.208.0/path/_common/glob_to_reg_exp.ts": "5c3c2b79fc2294ec803d102bd9855c451c150021f452046312819fbb6d4dc156",
|
||||
"https://deno.land/std@0.208.0/path/_common/normalize.ts": "2ba7fb4cc9fafb0f38028f434179579ce61d4d9e51296fad22b701c3d3cd7397",
|
||||
"https://deno.land/std@0.208.0/path/_common/normalize_string.ts": "88c472f28ae49525f9fe82de8c8816d93442d46a30d6bb5063b07ff8a89ff589",
|
||||
"https://deno.land/std@0.208.0/path/_common/relative.ts": "1af19d787a2a84b8c534cc487424fe101f614982ae4851382c978ab2216186b4",
|
||||
"https://deno.land/std@0.208.0/path/_common/strip_trailing_separators.ts": "7ffc7c287e97bdeeee31b155828686967f222cd73f9e5780bfe7dfb1b58c6c65",
|
||||
"https://deno.land/std@0.208.0/path/_common/to_file_url.ts": "a8cdd1633bc9175b7eebd3613266d7c0b6ae0fb0cff24120b6092ac31662f9ae",
|
||||
"https://deno.land/std@0.208.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b",
|
||||
"https://deno.land/std@0.208.0/path/_os.ts": "30b0c2875f360c9296dbe6b7f2d528f0f9c741cecad2e97f803f5219e91b40a2",
|
||||
"https://deno.land/std@0.208.0/path/basename.ts": "04bb5ef3e86bba8a35603b8f3b69537112cdd19ce64b77f2522006da2977a5f3",
|
||||
"https://deno.land/std@0.208.0/path/common.ts": "f4d061c7d0b95a65c2a1a52439edec393e906b40f1caf4604c389fae7caa80f5",
|
||||
"https://deno.land/std@0.208.0/path/dirname.ts": "88a0a71c21debafc4da7a4cd44fd32e899462df458fbca152390887d41c40361",
|
||||
"https://deno.land/std@0.208.0/path/extname.ts": "2da4e2490f3b48b7121d19fb4c91681a5e11bd6bd99df4f6f47d7a71bb6ecdf2",
|
||||
"https://deno.land/std@0.208.0/path/format.ts": "3457530cc85d1b4bab175f9ae73998b34fd456c830d01883169af0681b8894fb",
|
||||
"https://deno.land/std@0.208.0/path/from_file_url.ts": "e7fa233ea1dff9641e8d566153a24d95010110185a6f418dd2e32320926043f8",
|
||||
"https://deno.land/std@0.208.0/path/glob_to_regexp.ts": "74d7448c471e293d03f05ccb968df4365fed6aaa508506b6325a8efdc01d8271",
|
||||
"https://deno.land/std@0.208.0/path/is_absolute.ts": "67232b41b860571c5b7537f4954c88d86ae2ba45e883ee37d3dec27b74909d13",
|
||||
"https://deno.land/std@0.208.0/path/is_glob.ts": "567dce5c6656bdedfc6b3ee6c0833e1e4db2b8dff6e62148e94a917f289c06ad",
|
||||
"https://deno.land/std@0.208.0/path/join.ts": "98d3d76c819af4a11a81d5ba2dbb319f1ce9d63fc2b615597d4bcfddd4a89a09",
|
||||
"https://deno.land/std@0.208.0/path/join_globs.ts": "9b84d5103b63d3dbed4b2cf8b12477b2ad415c7d343f1488505162dc0e5f4db8",
|
||||
"https://deno.land/std@0.208.0/path/mod.ts": "3defabebc98279e62b392fee7a6937adc932a8f4dcd2471441e36c15b97b00e0",
|
||||
"https://deno.land/std@0.208.0/path/normalize.ts": "aa95be9a92c7bd4f9dc0ba51e942a1973e2b93d266cd74f5ca751c136d520b66",
|
||||
"https://deno.land/std@0.208.0/path/normalize_glob.ts": "674baa82e1c00b6cb153bbca36e06f8e0337cb8062db6d905ab5de16076ca46b",
|
||||
"https://deno.land/std@0.208.0/path/parse.ts": "d87ff0deef3fb495bc0d862278ff96da5a06acf0625ca27769fc52ac0d3d6ece",
|
||||
"https://deno.land/std@0.208.0/path/posix/_util.ts": "ecf49560fedd7dd376c6156cc5565cad97c1abe9824f4417adebc7acc36c93e5",
|
||||
"https://deno.land/std@0.208.0/path/posix/basename.ts": "a630aeb8fd8e27356b1823b9dedd505e30085015407caa3396332752f6b8406a",
|
||||
"https://deno.land/std@0.208.0/path/posix/common.ts": "e781d395dc76f6282e3f7dd8de13194abb8b04a82d109593141abc6e95755c8b",
|
||||
"https://deno.land/std@0.208.0/path/posix/dirname.ts": "f48c9c42cc670803b505478b7ef162c7cfa9d8e751b59d278b2ec59470531472",
|
||||
"https://deno.land/std@0.208.0/path/posix/extname.ts": "ee7f6571a9c0a37f9218fbf510c440d1685a7c13082c348d701396cc795e0be0",
|
||||
"https://deno.land/std@0.208.0/path/posix/format.ts": "b94876f77e61bfe1f147d5ccb46a920636cd3cef8be43df330f0052b03875968",
|
||||
"https://deno.land/std@0.208.0/path/posix/from_file_url.ts": "b97287a83e6407ac27bdf3ab621db3fccbf1c27df0a1b1f20e1e1b5acf38a379",
|
||||
"https://deno.land/std@0.208.0/path/posix/glob_to_regexp.ts": "6ed00c71fbfe0ccc35977c35444f94e82200b721905a60bd1278b1b768d68b1a",
|
||||
"https://deno.land/std@0.208.0/path/posix/is_absolute.ts": "159900a3422d11069d48395568217eb7fc105ceda2683d03d9b7c0f0769e01b8",
|
||||
"https://deno.land/std@0.208.0/path/posix/is_glob.ts": "ec4fbc604b9db8487f7b56ab0e759b24a971ab6a45f7b0b698bc39b8b9f9680f",
|
||||
"https://deno.land/std@0.208.0/path/posix/join.ts": "0c0d84bdc344876930126640011ec1b888e6facf74153ffad9ef26813aa2a076",
|
||||
"https://deno.land/std@0.208.0/path/posix/join_globs.ts": "f4838d54b1f60a34a40625a3293f6e583135348be1b2974341ac04743cb26121",
|
||||
"https://deno.land/std@0.208.0/path/posix/mod.ts": "f1b08a7f64294b7de87fc37190d63b6ce5b02889af9290c9703afe01951360ae",
|
||||
"https://deno.land/std@0.208.0/path/posix/normalize.ts": "11de90a94ab7148cc46e5a288f7d732aade1d616bc8c862f5560fa18ff987b4b",
|
||||
"https://deno.land/std@0.208.0/path/posix/normalize_glob.ts": "10a1840c628ebbab679254d5fa1c20e59106102354fb648a1765aed72eb9f3f9",
|
||||
"https://deno.land/std@0.208.0/path/posix/parse.ts": "199208f373dd93a792e9c585352bfc73a6293411bed6da6d3bc4f4ef90b04c8e",
|
||||
"https://deno.land/std@0.208.0/path/posix/relative.ts": "e2f230608b0f083e6deaa06e063943e5accb3320c28aef8d87528fbb7fe6504c",
|
||||
"https://deno.land/std@0.208.0/path/posix/resolve.ts": "51579d83159d5c719518c9ae50812a63959bbcb7561d79acbdb2c3682236e285",
|
||||
"https://deno.land/std@0.208.0/path/posix/separator.ts": "0b6573b5f3269a3164d8edc9cefc33a02dd51003731c561008c8bb60220ebac1",
|
||||
"https://deno.land/std@0.208.0/path/posix/to_file_url.ts": "08d43ea839ee75e9b8b1538376cfe95911070a655cd312bc9a00f88ef14967b6",
|
||||
"https://deno.land/std@0.208.0/path/posix/to_namespaced_path.ts": "c9228a0e74fd37e76622cd7b142b8416663a9b87db643302fa0926b5a5c83bdc",
|
||||
"https://deno.land/std@0.208.0/path/relative.ts": "23d45ede8b7ac464a8299663a43488aad6b561414e7cbbe4790775590db6349c",
|
||||
"https://deno.land/std@0.208.0/path/resolve.ts": "5b184efc87155a0af9fa305ff68a109e28de9aee81fc3e77cd01380f19daf867",
|
||||
"https://deno.land/std@0.208.0/path/separator.ts": "40a3e9a4ad10bef23bc2cd6c610291b6c502a06237c2c4cd034a15ca78dedc1f",
|
||||
"https://deno.land/std@0.208.0/path/to_file_url.ts": "edaafa089e0bce386e1b2d47afe7c72e379ff93b28a5829a5885e4b6c626d864",
|
||||
"https://deno.land/std@0.208.0/path/to_namespaced_path.ts": "cf8734848aac3c7527d1689d2adf82132b1618eff3cc523a775068847416b22a",
|
||||
"https://deno.land/std@0.208.0/path/windows/_util.ts": "f32b9444554c8863b9b4814025c700492a2b57ff2369d015360970a1b1099d54",
|
||||
"https://deno.land/std@0.208.0/path/windows/basename.ts": "8a9dbf7353d50afbc5b221af36c02a72c2d1b2b5b9f7c65bf6a5a2a0baf88ad3",
|
||||
"https://deno.land/std@0.208.0/path/windows/common.ts": "e781d395dc76f6282e3f7dd8de13194abb8b04a82d109593141abc6e95755c8b",
|
||||
"https://deno.land/std@0.208.0/path/windows/dirname.ts": "5c2aa541384bf0bd9aca821275d2a8690e8238fa846198ef5c7515ce31a01a94",
|
||||
"https://deno.land/std@0.208.0/path/windows/extname.ts": "07f4fa1b40d06a827446b3e3bcc8d619c5546b079b8ed0c77040bbef716c7614",
|
||||
"https://deno.land/std@0.208.0/path/windows/format.ts": "343019130d78f172a5c49fdc7e64686a7faf41553268961e7b6c92a6d6548edf",
|
||||
"https://deno.land/std@0.208.0/path/windows/from_file_url.ts": "d53335c12b0725893d768be3ac6bf0112cc5b639d2deb0171b35988493b46199",
|
||||
"https://deno.land/std@0.208.0/path/windows/glob_to_regexp.ts": "290755e18ec6c1a4f4d711c3390537358e8e3179581e66261a0cf348b1a13395",
|
||||
"https://deno.land/std@0.208.0/path/windows/is_absolute.ts": "245b56b5f355ede8664bd7f080c910a97e2169972d23075554ae14d73722c53c",
|
||||
"https://deno.land/std@0.208.0/path/windows/is_glob.ts": "ec4fbc604b9db8487f7b56ab0e759b24a971ab6a45f7b0b698bc39b8b9f9680f",
|
||||
"https://deno.land/std@0.208.0/path/windows/join.ts": "e6600bf88edeeef4e2276e155b8de1d5dec0435fd526ba2dc4d37986b2882f16",
|
||||
"https://deno.land/std@0.208.0/path/windows/join_globs.ts": "f4838d54b1f60a34a40625a3293f6e583135348be1b2974341ac04743cb26121",
|
||||
"https://deno.land/std@0.208.0/path/windows/mod.ts": "d7040f461465c2c21c1c68fc988ef0bdddd499912138cde3abf6ad60c7fb3814",
|
||||
"https://deno.land/std@0.208.0/path/windows/normalize.ts": "9deebbf40c81ef540b7b945d4ccd7a6a2c5a5992f791e6d3377043031e164e69",
|
||||
"https://deno.land/std@0.208.0/path/windows/normalize_glob.ts": "344ff5ed45430495b9a3d695567291e50e00b1b3b04ea56712a2acf07ab5c128",
|
||||
"https://deno.land/std@0.208.0/path/windows/parse.ts": "120faf778fe1f22056f33ded069b68e12447668fcfa19540c0129561428d3ae5",
|
||||
"https://deno.land/std@0.208.0/path/windows/relative.ts": "026855cd2c36c8f28f1df3c6fbd8f2449a2aa21f48797a74700c5d872b86d649",
|
||||
"https://deno.land/std@0.208.0/path/windows/resolve.ts": "5ff441ab18a2346abadf778121128ee71bda4d0898513d4639a6ca04edca366b",
|
||||
"https://deno.land/std@0.208.0/path/windows/separator.ts": "ae21f27015f10510ed1ac4a0ba9c4c9c967cbdd9d9e776a3e4967553c397bd5d",
|
||||
"https://deno.land/std@0.208.0/path/windows/to_file_url.ts": "8e9ea9e1ff364aa06fa72999204229952d0a279dbb876b7b838b2b2fea55cce3",
|
||||
"https://deno.land/std@0.208.0/path/windows/to_namespaced_path.ts": "e0f4d4a5e77f28a5708c1a33ff24360f35637ba6d8f103d19661255ef7bfd50d",
|
||||
"https://deno.land/std@0.208.0/testing/asserts.ts": "605bbd2ef0695e2a4324d810c4ad22e56041d51afb9584fc0b4e81084b14b1d6",
|
||||
"https://deno.land/std@0.213.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
|
||||
"https://deno.land/std@0.213.0/assert/_diff.ts": "dcc63d94ca289aec80644030cf88ccbf7acaa6fbd7b0f22add93616b36593840",
|
||||
"https://deno.land/std@0.213.0/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4",
|
||||
"https://deno.land/std@0.213.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5",
|
||||
"https://deno.land/std@0.213.0/assert/assert_almost_equals.ts": "8b96b7385cc117668b0720115eb6ee73d04c9bcb2f5d2344d674918c9113688f",
|
||||
"https://deno.land/std@0.213.0/assert/assert_array_includes.ts": "1688d76317fd45b7e93ef9e2765f112fdf2b7c9821016cdfb380b9445374aed1",
|
||||
"https://deno.land/std@0.213.0/assert/assert_equals.ts": "4497c56fe7d2993b0d447926702802fc0becb44e319079e8eca39b482ee01b4e",
|
||||
"https://deno.land/std@0.213.0/assert/assert_exists.ts": "24a7bf965e634f909242cd09fbaf38bde6b791128ece08e33ab08586a7cc55c9",
|
||||
"https://deno.land/std@0.213.0/assert/assert_false.ts": "6f382568e5128c0f855e5f7dbda8624c1ed9af4fcc33ef4a9afeeedcdce99769",
|
||||
"https://deno.land/std@0.213.0/assert/assert_greater.ts": "4945cf5729f1a38874d7e589e0fe5cc5cd5abe5573ca2ddca9d3791aa891856c",
|
||||
"https://deno.land/std@0.213.0/assert/assert_greater_or_equal.ts": "573ed8823283b8d94b7443eb69a849a3c369a8eb9666b2d1db50c33763a5d219",
|
||||
"https://deno.land/std@0.213.0/assert/assert_instance_of.ts": "72dc1faff1e248692d873c89382fa1579dd7b53b56d52f37f9874a75b11ba444",
|
||||
"https://deno.land/std@0.213.0/assert/assert_is_error.ts": "6596f2b5ba89ba2fe9b074f75e9318cda97a2381e59d476812e30077fbdb6ed2",
|
||||
"https://deno.land/std@0.213.0/assert/assert_less.ts": "2b4b3fe7910f65f7be52212f19c3977ecb8ba5b2d6d0a296c83cde42920bb005",
|
||||
"https://deno.land/std@0.213.0/assert/assert_less_or_equal.ts": "b93d212fe669fbde959e35b3437ac9a4468f2e6b77377e7b6ea2cfdd825d38a0",
|
||||
"https://deno.land/std@0.213.0/assert/assert_match.ts": "ec2d9680ed3e7b9746ec57ec923a17eef6d476202f339ad91d22277d7f1d16e1",
|
||||
"https://deno.land/std@0.213.0/assert/assert_not_equals.ts": "f3edda73043bc2c9fae6cbfaa957d5c69bbe76f5291a5b0466ed132c8789df4c",
|
||||
"https://deno.land/std@0.213.0/assert/assert_not_instance_of.ts": "8f720d92d83775c40b2542a8d76c60c2d4aeddaf8713c8d11df8984af2604931",
|
||||
"https://deno.land/std@0.213.0/assert/assert_not_match.ts": "b4b7c77f146963e2b673c1ce4846473703409eb93f5ab0eb60f6e6f8aeffe39f",
|
||||
"https://deno.land/std@0.213.0/assert/assert_not_strict_equals.ts": "da0b8ab60a45d5a9371088378e5313f624799470c3b54c76e8b8abeec40a77be",
|
||||
"https://deno.land/std@0.213.0/assert/assert_object_match.ts": "e85e5eef62a56ce364c3afdd27978ccab979288a3e772e6855c270a7b118fa49",
|
||||
"https://deno.land/std@0.213.0/assert/assert_rejects.ts": "e9e0c8d9c3e164c7ac962c37b3be50577c5a2010db107ed272c4c1afb1269f54",
|
||||
"https://deno.land/std@0.213.0/assert/assert_strict_equals.ts": "0425a98f70badccb151644c902384c12771a93e65f8ff610244b8147b03a2366",
|
||||
"https://deno.land/std@0.213.0/assert/assert_string_includes.ts": "dfb072a890167146f8e5bdd6fde887ce4657098e9f71f12716ef37f35fb6f4a7",
|
||||
"https://deno.land/std@0.213.0/assert/assert_throws.ts": "edddd86b39606c342164b49ad88dd39a26e72a26655e07545d172f164b617fa7",
|
||||
"https://deno.land/std@0.213.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8",
|
||||
"https://deno.land/std@0.213.0/assert/equal.ts": "fae5e8a52a11d3ac694bbe1a53e13a7969e3f60791262312e91a3e741ae519e2",
|
||||
"https://deno.land/std@0.213.0/assert/fail.ts": "f310e51992bac8e54f5fd8e44d098638434b2edb802383690e0d7a9be1979f1c",
|
||||
"https://deno.land/std@0.213.0/assert/mod.ts": "325df8c0683ad83a873b9691aa66b812d6275fc9fec0b2d180ac68a2c5efed3b",
|
||||
"https://deno.land/std@0.213.0/assert/unimplemented.ts": "47ca67d1c6dc53abd0bd729b71a31e0825fc452dbcd4fde4ca06789d5644e7fd",
|
||||
"https://deno.land/std@0.213.0/assert/unreachable.ts": "38cfecb95d8b06906022d2f9474794fca4161a994f83354fd079cac9032b5145",
|
||||
"https://deno.land/std@0.213.0/fmt/colors.ts": "aeaee795471b56fc62a3cb2e174ed33e91551b535f44677f6320336aabb54fbb",
|
||||
"https://deno.land/std@0.213.0/testing/_test_suite.ts": "f10a8a6338b60c403f07a76f3f46bdc9f1e1a820c0a1decddeb2949f7a8a0546",
|
||||
"https://deno.land/std@0.213.0/testing/bdd.ts": "3cbd17bd35f629a76ce63446238dfb4632240dd46b3b205027c45fa3dd67e554",
|
||||
"https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
|
||||
"https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
|
||||
"https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293",
|
||||
"https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7",
|
||||
"https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74",
|
||||
"https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd",
|
||||
"https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff",
|
||||
"https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46",
|
||||
"https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b",
|
||||
"https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c",
|
||||
"https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491",
|
||||
"https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68",
|
||||
"https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3",
|
||||
"https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8",
|
||||
"https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693",
|
||||
"https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31",
|
||||
"https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5",
|
||||
"https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8",
|
||||
"https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb",
|
||||
"https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
|
||||
"https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47",
|
||||
"https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68",
|
||||
"https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3",
|
||||
"https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73",
|
||||
"https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19",
|
||||
"https://deno.land/std@0.224.0/cli/parse_args.ts": "5250832fb7c544d9111e8a41ad272c016f5a53f975ef84d5a9fe5fcb70566ece",
|
||||
"https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5",
|
||||
"https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6",
|
||||
"https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2",
|
||||
"https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e",
|
||||
"https://deno.land/std@0.224.0/yaml/_dumper/dumper.ts": "08b595b40841a2e1c75303f5096392323b6baf8e9662430a91e3b36fbe175fe9",
|
||||
"https://deno.land/std@0.224.0/yaml/_dumper/dumper_state.ts": "9e29f700ea876ed230b43f11fa006fcb1a62eedc1e27d32baaeaf3210f19f1e7",
|
||||
"https://deno.land/std@0.224.0/yaml/_error.ts": "f38cdebdb69cde16903d9aa2f3b8a3dd9d13e5f7f3570bf662bfaca69fef669e",
|
||||
"https://deno.land/std@0.224.0/yaml/_loader/loader.ts": "bf9e8a99770b59bc887b43ebccea108cbe9146ae32d91f7ce558d62c946d3fe3",
|
||||
"https://deno.land/std@0.224.0/yaml/_loader/loader_state.ts": "ee216de6040551940b85473c3185fdb7a6f3030b77153f87a6b7f63f82e489ea",
|
||||
"https://deno.land/std@0.224.0/yaml/_mark.ts": "61097a614857fcebf7b2ecad057916d74c90cd160117a33c9e74bac60457410a",
|
||||
"https://deno.land/std@0.224.0/yaml/_state.ts": "f3b1c1fd11860302f1f33e35e9ce089bf069d4943e8d67516cd6bedbba058c13",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/binary.ts": "f1a6e1d83dcc52b21cc3639cd98be44051cfc54065cc4f2a42065bce07ebc07d",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/bool.ts": "121743b23ba82a27ad6a3ec6298c7f5b0908f90e52707f8644a91f7ad51ed2ef",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/float.ts": "c5ed84b0aec1ec5dc05f6abfaaff672e8890d4d44a42120b4445c9754fca4eba",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/function.ts": "bbf705058942bf3370604b37eb77a10aadd72f986c237c9f69b43378a42202c1",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/int.ts": "c2dc88438a60fccc8d2226042bd18b9967753adaf6bd145feb8b99d567e432ce",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/map.ts": "ae2acb1cb837fb8e96c75c98611cfd45af847d0114ab5336333c318e7d4b12f4",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/merge.ts": "ad0d971f91d2fb9f4ab3eba0c837eae357b1804d6b798adc99dc917bc5306b11",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/mod.ts": "e8929d7b1c969a74f76338d4eb380ef8c4a26cd6441117d521f076b766e9c265",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/nil.ts": "cbe4387d02d5933322c21b25d8955c5e6228c492e391a6fb82dcf4f498cc421c",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/omap.ts": "cda915105ab22ba9e1d6317adacee8eec2d8ddaf864cc2f814e3e476946e72c6",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/pairs.ts": "dd39bb44c1b9abaf6172c63f73350475933151f07e05253b81f7860c9b507177",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/regexp.ts": "e49eb9e1c9356fd142bc15f7f323820d411fcc537b5ba3896df9a8b812d270a4",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/seq.ts": "2deffc7f970869bc01a1541b4961d076329a1c2b30b95e07918f3132db7c3fe2",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/set.ts": "be8a9e7237a7ffc92dfbe7f5e552d84b7eeba60f3f73cc77fc3c59d3506c74ea",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/str.ts": "88f0a1ba12295520cd57e96cd78d53aa0787d53c7a1c506155f418c496c2f550",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/timestamp.ts": "277a41a40fb93c3b2b3f5c373bf11b0b7856cc6a7b919e8ea130755e4029edc5",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/undefined.ts": "9d215953c65740f1764e0bdca021007573473f0c49e087f00d9ff02817ecfc97",
|
||||
"https://deno.land/std@0.224.0/yaml/_utils.ts": "91bbe28b5e7000b9594e40ff5353f8fe7a7ba914eec917e1202cbaf5ac931c58",
|
||||
"https://deno.land/std@0.224.0/yaml/mod.ts": "54e9bfad77c8cd58f49b65f4d568045ff08989ed36318a2ca733a43cb6f1bc00",
|
||||
"https://deno.land/std@0.224.0/yaml/parse.ts": "f45278d9ebccb789af4eceeffa5c291e194bcf1fa9aab1b34ff52c2bd4a9d886",
|
||||
"https://deno.land/std@0.224.0/yaml/schema.ts": "a0f7956d997852b5d1c6564bd73eb7352175cfba439707ac819b65b5a2ec173a",
|
||||
"https://deno.land/std@0.224.0/yaml/schema/core.ts": "0a37c07710e3df4eb4edc02f4edf623bf8df5af72b34d8a7c0229d0bac2a7043",
|
||||
"https://deno.land/std@0.224.0/yaml/schema/default.ts": "1367fd30420c7071ecc67e5b470838474e8259aaf64460f314af4b6bd8da497c",
|
||||
"https://deno.land/std@0.224.0/yaml/schema/extended.ts": "248180c22697f37ed173057eae62ce4879865bb59f30c4908d698bed5edcc7c5",
|
||||
"https://deno.land/std@0.224.0/yaml/schema/failsafe.ts": "0ac1cae5b86d8fe2c83ad0a17f8adc33106a452b7139f84e4b0bfaee2206730e",
|
||||
"https://deno.land/std@0.224.0/yaml/schema/json.ts": "a0228a0c0bad7dece17ab848774fcadc2ccb5e51775c2d58d21d486917ba3ba1",
|
||||
"https://deno.land/std@0.224.0/yaml/schema/mod.ts": "0e1558a4823834f106675e48ddc15338e04f6f18469d1a7d6b3f0e1ab06abcb2",
|
||||
"https://deno.land/std@0.224.0/yaml/stringify.ts": "f0ed4e419cb40c807cf79ae4039d6cdf492be9a947121fff4d4b7cd1d4738bae",
|
||||
"https://deno.land/std@0.224.0/yaml/type.ts": "708dde5f20b01cc1096489b7155b6af79a217d585afb841128e78c3c2391eb5c"
|
||||
},
|
||||
"workspace": {
|
||||
"dependencies": [
|
||||
"jsr:@deno/dnt@~0.41.3",
|
||||
|
||||
+2
-1
@@ -13,7 +13,8 @@ await build({
|
||||
},
|
||||
],
|
||||
outDir: "./npm",
|
||||
shims: {
|
||||
test: false, // Disable all tests in npm build since they use Deno-specific APIs
|
||||
shims: {
|
||||
// see JS docs for overview and more options
|
||||
deno: true,
|
||||
// shims to only use in the tests
|
||||
|
||||
+7
-1
@@ -27,7 +27,13 @@ export function replaceInlineScripts(
|
||||
localPath: string,
|
||||
removeLocks: string[] | undefined
|
||||
) {
|
||||
modules.forEach((m) => {
|
||||
modules.forEach((m, i) => {
|
||||
if (!m.value) {
|
||||
throw Error(
|
||||
`Module value is undefined for flow module ${i} in ${localPath}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (m.value.type == "rawscript") {
|
||||
if (m.value.content.startsWith("!inline")) {
|
||||
const path = m.value.content.split(" ")[1];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -365,6 +365,7 @@ async function instancePull(opts: InstanceSyncOptions) {
|
||||
workspace: workspaceName,
|
||||
token: undefined,
|
||||
baseUrl: undefined,
|
||||
configDir: undefined,
|
||||
includeGroups: true,
|
||||
includeSchedules: true,
|
||||
includeTriggers: true,
|
||||
@@ -515,6 +516,7 @@ async function instancePush(opts: InstanceSyncOptions) {
|
||||
token: instance.token,
|
||||
workspace: undefined,
|
||||
baseUrl: undefined,
|
||||
configDir: undefined,
|
||||
create: true,
|
||||
createWorkspaceName: workspaceSettings.name,
|
||||
createUsername: undefined,
|
||||
@@ -533,6 +535,7 @@ async function instancePush(opts: InstanceSyncOptions) {
|
||||
workspace: localWorkspace.dir,
|
||||
token: undefined,
|
||||
baseUrl: undefined,
|
||||
configDir: undefined,
|
||||
includeGroups: true,
|
||||
includeSchedules: true,
|
||||
includeTriggers: true,
|
||||
|
||||
+348
-195
@@ -1,11 +1,11 @@
|
||||
import {
|
||||
Command,
|
||||
CompletionsCommand,
|
||||
UpgradeCommand,
|
||||
colors,
|
||||
esMain,
|
||||
log,
|
||||
yamlStringify,
|
||||
Command,
|
||||
CompletionsCommand,
|
||||
UpgradeCommand,
|
||||
colors,
|
||||
esMain,
|
||||
log,
|
||||
yamlStringify,
|
||||
} from "./deps.ts";
|
||||
import flow from "./flow.ts";
|
||||
import app from "./apps.ts";
|
||||
@@ -19,6 +19,7 @@ import folder from "./folder.ts";
|
||||
import schedule from "./schedule.ts";
|
||||
import trigger from "./trigger.ts";
|
||||
import sync from "./sync.ts";
|
||||
import gitsyncSettings from "./gitsync-settings.ts";
|
||||
import instance from "./instance.ts";
|
||||
import workerGroups from "./worker_groups.ts";
|
||||
|
||||
@@ -36,24 +37,25 @@ import queues from "./queues.ts";
|
||||
import { readLockfile } from "./metadata.ts";
|
||||
|
||||
export {
|
||||
flow,
|
||||
app,
|
||||
script,
|
||||
workspace,
|
||||
resource,
|
||||
user,
|
||||
variable,
|
||||
hub,
|
||||
folder,
|
||||
schedule,
|
||||
trigger,
|
||||
sync,
|
||||
instance,
|
||||
dev,
|
||||
hubPull,
|
||||
pull,
|
||||
push,
|
||||
workspaceAdd,
|
||||
flow,
|
||||
app,
|
||||
script,
|
||||
workspace,
|
||||
resource,
|
||||
user,
|
||||
variable,
|
||||
hub,
|
||||
folder,
|
||||
schedule,
|
||||
trigger,
|
||||
sync,
|
||||
gitsyncSettings,
|
||||
instance,
|
||||
dev,
|
||||
hubPull,
|
||||
pull,
|
||||
push,
|
||||
workspaceAdd,
|
||||
};
|
||||
|
||||
// addEventListener("error", (event) => {
|
||||
@@ -63,201 +65,352 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.505.3";
|
||||
export const VERSION = "1.507.0";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
.action(() =>
|
||||
log.info(`Welcome to Windmill CLI ${VERSION}. Use -h for help.`)
|
||||
)
|
||||
.description("Windmill CLI")
|
||||
.name("wmill")
|
||||
.action(() =>
|
||||
log.info(`Welcome to Windmill CLI ${VERSION}. Use -h for help.`),
|
||||
)
|
||||
.description("Windmill CLI")
|
||||
|
||||
.globalOption(
|
||||
"--workspace <workspace:string>",
|
||||
"Specify the target workspace. This overrides the default workspace."
|
||||
)
|
||||
.globalOption("--debug --verbose", "Show debug/verbose logs")
|
||||
.globalOption(
|
||||
"--show-diffs",
|
||||
"Show diff informations when syncing (may show sensitive informations)"
|
||||
)
|
||||
.globalOption(
|
||||
"--token <token:string>",
|
||||
"Specify an API token. This will override any stored token."
|
||||
)
|
||||
.globalOption(
|
||||
"--base-url <baseUrl:string>",
|
||||
"Specify the base URL of the API. If used, --token and --workspace are required and no local remote/workspace already set will be used."
|
||||
)
|
||||
.env(
|
||||
"HEADERS <headers:string>",
|
||||
"Specify headers to use for all requests. e.g: \"HEADERS='h1: v1, h2: v2'\""
|
||||
)
|
||||
.version(VERSION)
|
||||
.versionOption(false)
|
||||
.command("init", "Bootstrap a windmill project with a wmill.yaml file")
|
||||
.action(async () => {
|
||||
if (await Deno.stat("wmill.yaml").catch(() => null)) {
|
||||
log.error(colors.green("wmill.yaml already exists"));
|
||||
} else {
|
||||
await Deno.writeTextFile(
|
||||
"wmill.yaml",
|
||||
yamlStringify({
|
||||
defaultTs: "bun",
|
||||
includes: ["f/**"],
|
||||
excludes: [],
|
||||
codebases: [],
|
||||
skipVariables: true,
|
||||
skipResources: true,
|
||||
skipSecrets: true,
|
||||
includeSchedules: false,
|
||||
includeTriggers: false,
|
||||
})
|
||||
);
|
||||
log.info(colors.green("wmill.yaml created"));
|
||||
}
|
||||
await readLockfile();
|
||||
})
|
||||
.command("app", app)
|
||||
.command("flow", flow)
|
||||
.command("script", script)
|
||||
.command("workspace", workspace)
|
||||
.command("resource", resource)
|
||||
.command("user", user)
|
||||
.command("variable", variable)
|
||||
.command("hub", hub)
|
||||
.command("folder", folder)
|
||||
.command("schedule", schedule)
|
||||
.command("trigger", trigger)
|
||||
.command("dev", dev)
|
||||
.command("sync", sync)
|
||||
.command("instance", instance)
|
||||
.command("worker-groups", workerGroups)
|
||||
.command("workers", workers)
|
||||
.command("queues", queues)
|
||||
.command("version --version", "Show version information")
|
||||
.action(async (opts) => {
|
||||
console.log("CLI version: " + VERSION);
|
||||
try {
|
||||
const provider = new NpmProvider({ package: "windmill-cli" });
|
||||
const versions = await provider.getVersions("windmill-cli");
|
||||
if (versions.latest !== VERSION) {
|
||||
console.log(
|
||||
`CLI is outdated. Latest version ${versions.latest} is available. Run \`wmill upgrade\` to update.`
|
||||
);
|
||||
} else {
|
||||
console.log("CLI is up to date");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`Cannot fetch latest CLI version on npmjs to check if up-to-date: ${e}`
|
||||
);
|
||||
}
|
||||
const workspace = await getActiveWorkspace(opts as GlobalOptions);
|
||||
if (workspace) {
|
||||
try {
|
||||
const backendVersion = await fetchVersion(workspace.remote);
|
||||
console.log("Backend Version: " + backendVersion);
|
||||
} catch (e) {
|
||||
console.warn("Cannot fetch backend version: " + e);
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
"Cannot fetch backend version: no active workspace selected, choose one to pick a remote to fetch version of"
|
||||
);
|
||||
}
|
||||
})
|
||||
.command(
|
||||
"upgrade",
|
||||
new UpgradeCommand({
|
||||
provider: new NpmProvider({ package: "windmill-cli" }),
|
||||
}).error((e) => {
|
||||
log.error(e);
|
||||
log.info(
|
||||
"Try running with sudo and otherwise check the result of the command: npm uninstall windmill-cli && npm install -g windmill-cli"
|
||||
);
|
||||
.globalOption(
|
||||
"--workspace <workspace:string>",
|
||||
"Specify the target workspace. This overrides the default workspace.",
|
||||
)
|
||||
.globalOption("--debug --verbose", "Show debug/verbose logs")
|
||||
.globalOption(
|
||||
"--show-diffs",
|
||||
"Show diff informations when syncing (may show sensitive informations)",
|
||||
)
|
||||
.globalOption(
|
||||
"--token <token:string>",
|
||||
"Specify an API token. This will override any stored token.",
|
||||
)
|
||||
.globalOption(
|
||||
"--base-url <baseUrl:string>",
|
||||
"Specify the base URL of the API. If used, --token and --workspace are required and no local remote/workspace already set will be used.",
|
||||
)
|
||||
.globalOption(
|
||||
"--config-dir <configDir:string>",
|
||||
"Specify a custom config directory. Overrides WMILL_CONFIG_DIR environment variable and default ~/.config location.",
|
||||
)
|
||||
.env(
|
||||
"HEADERS <headers:string>",
|
||||
"Specify headers to use for all requests. e.g: \"HEADERS='h1: v1, h2: v2'\"",
|
||||
)
|
||||
.version(VERSION)
|
||||
.versionOption(false)
|
||||
.command("init", "Bootstrap a windmill project with a wmill.yaml file")
|
||||
.option("--use-default", "Use default settings without checking backend")
|
||||
.option("--use-backend", "Use backend git-sync settings if available")
|
||||
.option(
|
||||
"--repository <repo:string>",
|
||||
"Specify repository path (e.g., u/user/repo) when using backend settings",
|
||||
)
|
||||
.action(
|
||||
async (
|
||||
opts: {
|
||||
useDefault?: boolean;
|
||||
useBackend?: boolean;
|
||||
repository?: string;
|
||||
workspace?: string;
|
||||
debug?: unknown;
|
||||
showDiffs?: boolean;
|
||||
token?: string;
|
||||
baseUrl?: string;
|
||||
configDir?: string;
|
||||
},
|
||||
) => {
|
||||
if (await Deno.stat("wmill.yaml").catch(() => null)) {
|
||||
log.error(colors.red("wmill.yaml already exists"));
|
||||
} else {
|
||||
// Import DEFAULT_SYNC_OPTIONS from conf.ts
|
||||
const { DEFAULT_SYNC_OPTIONS } = await import("./conf.ts");
|
||||
|
||||
// Create initial config with defaults
|
||||
const initialConfig = {
|
||||
defaultTs: DEFAULT_SYNC_OPTIONS.defaultTs,
|
||||
includes: DEFAULT_SYNC_OPTIONS.includes,
|
||||
excludes: DEFAULT_SYNC_OPTIONS.excludes,
|
||||
codebases: DEFAULT_SYNC_OPTIONS.codebases,
|
||||
skipVariables: DEFAULT_SYNC_OPTIONS.skipVariables,
|
||||
skipResources: DEFAULT_SYNC_OPTIONS.skipResources,
|
||||
skipSecrets: DEFAULT_SYNC_OPTIONS.skipSecrets,
|
||||
skipScripts: DEFAULT_SYNC_OPTIONS.skipScripts,
|
||||
skipFlows: DEFAULT_SYNC_OPTIONS.skipFlows,
|
||||
skipApps: DEFAULT_SYNC_OPTIONS.skipApps,
|
||||
skipFolders: DEFAULT_SYNC_OPTIONS.skipFolders,
|
||||
includeSchedules: DEFAULT_SYNC_OPTIONS.includeSchedules,
|
||||
includeTriggers: DEFAULT_SYNC_OPTIONS.includeTriggers,
|
||||
};
|
||||
|
||||
await Deno.writeTextFile(
|
||||
"wmill.yaml",
|
||||
yamlStringify(initialConfig),
|
||||
);
|
||||
log.info(
|
||||
colors.green("wmill.yaml created with default settings"),
|
||||
);
|
||||
|
||||
// Create lock file
|
||||
await readLockfile();
|
||||
|
||||
// Check for backend git-sync settings unless --use-default is specified
|
||||
if (!opts.useDefault) {
|
||||
try {
|
||||
const { requireLogin, resolveWorkspace } = await import(
|
||||
"./context.ts"
|
||||
);
|
||||
|
||||
// Check if user has workspace configured
|
||||
const { getActiveWorkspace } = await import(
|
||||
"./workspace.ts"
|
||||
);
|
||||
const activeWorkspace = await getActiveWorkspace(opts as GlobalOptions);
|
||||
|
||||
if (!activeWorkspace) {
|
||||
log.info(
|
||||
"No workspace configured. Using default settings.",
|
||||
);
|
||||
log.info(
|
||||
"You can configure a workspace later with 'wmill workspace add'",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await requireLogin(opts as GlobalOptions);
|
||||
const workspace = await resolveWorkspace(opts as GlobalOptions);
|
||||
|
||||
const wmill = await import("./gen/services.gen.ts");
|
||||
const settings = await wmill.getSettings({
|
||||
workspace: workspace.workspaceId,
|
||||
});
|
||||
|
||||
if (
|
||||
settings.git_sync?.repositories &&
|
||||
settings.git_sync.repositories.length > 0
|
||||
) {
|
||||
let useBackendSettings = opts.useBackend;
|
||||
|
||||
// If repository is specified, implicitly use backend settings
|
||||
if (opts.repository && !opts.useDefault) {
|
||||
useBackendSettings = true;
|
||||
}
|
||||
|
||||
if (useBackendSettings === undefined) {
|
||||
// Interactive prompt
|
||||
const { Select } = await import("./deps.ts");
|
||||
const choice = await Select.prompt({
|
||||
message:
|
||||
"Git-sync settings found on backend. What would you like to do?",
|
||||
options: [
|
||||
{
|
||||
name: "Use backend git-sync settings",
|
||||
value: "backend",
|
||||
},
|
||||
{
|
||||
name: "Use default settings",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
name: "Cancel",
|
||||
value: "cancel",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (choice === "cancel") {
|
||||
// Clean up the created files
|
||||
try {
|
||||
await Deno.remove("wmill.yaml");
|
||||
await Deno.remove("wmill-lock.yaml");
|
||||
} catch (e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
log.info("Init cancelled");
|
||||
Deno.exit(0);
|
||||
}
|
||||
|
||||
useBackendSettings = choice === "backend";
|
||||
}
|
||||
|
||||
if (useBackendSettings) {
|
||||
log.info(
|
||||
"Applying git-sync settings from backend...",
|
||||
);
|
||||
|
||||
// Import and run the pull git-sync settings logic
|
||||
const { pullGitSyncSettings } = await import(
|
||||
"./gitsync-settings.ts"
|
||||
);
|
||||
await pullGitSyncSettings({
|
||||
...(opts as GlobalOptions),
|
||||
repository: opts.repository,
|
||||
jsonOutput: false,
|
||||
diff: false,
|
||||
replace: true, // Auto-replace when using backend settings during init
|
||||
});
|
||||
|
||||
log.info(
|
||||
colors.green(
|
||||
"Git-sync settings applied from backend",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// If there's an error checking backend settings, just continue with defaults
|
||||
log.warn(
|
||||
`Could not check backend for git-sync settings: ${error.message}`,
|
||||
);
|
||||
log.info("Continuing with default settings");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.command("app", app)
|
||||
.command("flow", flow)
|
||||
.command("script", script)
|
||||
.command("workspace", workspace)
|
||||
.command("resource", resource)
|
||||
.command("user", user)
|
||||
.command("variable", variable)
|
||||
.command("hub", hub)
|
||||
.command("folder", folder)
|
||||
.command("schedule", schedule)
|
||||
.command("trigger", trigger)
|
||||
.command("dev", dev)
|
||||
.command("sync", sync)
|
||||
.command("gitsync-settings", gitsyncSettings)
|
||||
.command("instance", instance)
|
||||
.command("worker-groups", workerGroups)
|
||||
.command("workers", workers)
|
||||
.command("queues", queues)
|
||||
.command("version --version", "Show version information")
|
||||
.action(async (opts) => {
|
||||
console.log("CLI version: " + VERSION);
|
||||
try {
|
||||
const provider = new NpmProvider({ package: "windmill-cli" });
|
||||
const versions = await provider.getVersions("windmill-cli");
|
||||
if (versions.latest !== VERSION) {
|
||||
console.log(
|
||||
`CLI is outdated. Latest version ${versions.latest} is available. Run \`wmill upgrade\` to update.`,
|
||||
);
|
||||
} else {
|
||||
console.log("CLI is up to date");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`Cannot fetch latest CLI version on npmjs to check if up-to-date: ${e}`,
|
||||
);
|
||||
}
|
||||
const workspace = await getActiveWorkspace(opts as GlobalOptions);
|
||||
if (workspace) {
|
||||
try {
|
||||
const backendVersion = await fetchVersion(workspace.remote);
|
||||
console.log("Backend Version: " + backendVersion);
|
||||
} catch (e) {
|
||||
console.warn("Cannot fetch backend version: " + e);
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
"Cannot fetch backend version: no active workspace selected, choose one to pick a remote to fetch version of",
|
||||
);
|
||||
}
|
||||
})
|
||||
)
|
||||
.command("completions", new CompletionsCommand());
|
||||
.command(
|
||||
"upgrade",
|
||||
new UpgradeCommand({
|
||||
provider: new NpmProvider({ package: "windmill-cli" }),
|
||||
}).error((e) => {
|
||||
log.error(e);
|
||||
log.info(
|
||||
"Try running with sudo and otherwise check the result of the command: npm uninstall windmill-cli && npm install -g windmill-cli",
|
||||
);
|
||||
}),
|
||||
)
|
||||
.command("completions", new CompletionsCommand());
|
||||
|
||||
export let showDiffs = false;
|
||||
|
||||
let isWin: boolean | undefined = undefined;
|
||||
|
||||
export async function getIsWin() {
|
||||
if (isWin === undefined) {
|
||||
const os = await import("node:os");
|
||||
isWin = os.platform() === "win32";
|
||||
}
|
||||
return isWin;
|
||||
if (isWin === undefined) {
|
||||
const os = await import("node:os");
|
||||
isWin = os.platform() === "win32";
|
||||
}
|
||||
return isWin;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
if (Deno.args.length === 0) {
|
||||
command.showHelp();
|
||||
}
|
||||
const LOG_LEVEL =
|
||||
Deno.args.includes("--verbose") || Deno.args.includes("--debug")
|
||||
? "DEBUG"
|
||||
: "INFO";
|
||||
// const NO_COLORS = Deno.args.includes("--no-colors");
|
||||
showDiffs = Deno.args.includes("--show-diffs");
|
||||
try {
|
||||
if (Deno.args.length === 0) {
|
||||
command.showHelp();
|
||||
}
|
||||
const LOG_LEVEL =
|
||||
Deno.args.includes("--verbose") || Deno.args.includes("--debug")
|
||||
? "DEBUG"
|
||||
: "INFO";
|
||||
// const NO_COLORS = Deno.args.includes("--no-colors");
|
||||
showDiffs = Deno.args.includes("--show-diffs");
|
||||
|
||||
log.setup({
|
||||
handlers: {
|
||||
console: new log.ConsoleHandler(LOG_LEVEL, {
|
||||
formatter: ({ msg }) => `${msg}`,
|
||||
useColors: isWin ? false : true,
|
||||
}),
|
||||
},
|
||||
loggers: {
|
||||
default: {
|
||||
level: LOG_LEVEL,
|
||||
handlers: ["console"],
|
||||
},
|
||||
},
|
||||
});
|
||||
log.debug("Debug logging enabled. CLI build against " + VERSION);
|
||||
log.setup({
|
||||
handlers: {
|
||||
console: new log.ConsoleHandler(LOG_LEVEL, {
|
||||
formatter: ({ msg }) => `${msg}`,
|
||||
useColors: isWin ? false : true,
|
||||
}),
|
||||
},
|
||||
loggers: {
|
||||
default: {
|
||||
level: LOG_LEVEL,
|
||||
handlers: ["console"],
|
||||
},
|
||||
},
|
||||
});
|
||||
log.debug("Debug logging enabled. CLI build against " + VERSION);
|
||||
|
||||
const extraHeaders = getHeaders();
|
||||
if (extraHeaders) {
|
||||
OpenAPI.HEADERS = extraHeaders;
|
||||
const extraHeaders = getHeaders();
|
||||
if (extraHeaders) {
|
||||
OpenAPI.HEADERS = extraHeaders;
|
||||
}
|
||||
await command.parse(Deno.args);
|
||||
} catch (e) {
|
||||
if (e.name === "ApiError") {
|
||||
console.log("Server failed. " + e.statusText + ": " + e.body);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
await command.parse(Deno.args);
|
||||
} catch (e) {
|
||||
if (e.name === "ApiError") {
|
||||
console.log("Server failed. " + e.statusText + ": " + e.body);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function isMain() {
|
||||
// dnt-shim-ignore
|
||||
const { Deno } = globalThis as any;
|
||||
// dnt-shim-ignore
|
||||
const { Deno } = globalThis as any;
|
||||
|
||||
const isDeno = Deno != undefined;
|
||||
const isDeno = Deno != undefined;
|
||||
|
||||
if (isDeno) {
|
||||
const isMain = import.meta.main;
|
||||
if (isMain) {
|
||||
if (!Deno.args.includes("completions")) {
|
||||
if (Deno.env.get("SKIP_DENO_DEPRECATION_WARNING") !== "true") {
|
||||
log.warn(
|
||||
"Using the deno runtime for the Windmill CLI is deprecated, you can now use node: deno uninstall wmill && npm install -g windmill-cli. To skip this warning set SKIP_DENO_DEPRECATION_WARNING=true"
|
||||
);
|
||||
if (isDeno) {
|
||||
const isMain = import.meta.main;
|
||||
if (isMain) {
|
||||
if (!Deno.args.includes("completions")) {
|
||||
if (Deno.env.get("SKIP_DENO_DEPRECATION_WARNING") !== "true") {
|
||||
log.warn(
|
||||
"Using the deno runtime for the Windmill CLI is deprecated, you can now use node: deno uninstall wmill && npm install -g windmill-cli. To skip this warning set SKIP_DENO_DEPRECATION_WARNING=true",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return isMain;
|
||||
} else {
|
||||
//@ts-ignore
|
||||
return esMain.default(import.meta);
|
||||
}
|
||||
return isMain;
|
||||
} else {
|
||||
//@ts-ignore
|
||||
return esMain.default(import.meta);
|
||||
}
|
||||
}
|
||||
if (isMain()) {
|
||||
main();
|
||||
main();
|
||||
}
|
||||
|
||||
export default command;
|
||||
|
||||
+9
-4
@@ -13,15 +13,20 @@ function hash_string(str: string): number {
|
||||
return hash;
|
||||
}
|
||||
|
||||
export async function getRootStore(): Promise<string> {
|
||||
const store = (config_dir() ?? tmp_dir() ?? "/tmp/") + "/windmill/";
|
||||
export async function getRootStore(configDirOverride?: string): Promise<string> {
|
||||
const baseDir = configDirOverride ??
|
||||
Deno.env.get("WMILL_CONFIG_DIR") ??
|
||||
config_dir() ??
|
||||
tmp_dir() ??
|
||||
"/tmp/";
|
||||
const store = baseDir + "/windmill/";
|
||||
await ensureDir(store);
|
||||
return store;
|
||||
}
|
||||
|
||||
export async function getStore(baseUrl: string): Promise<string> {
|
||||
export async function getStore(baseUrl: string, configDirOverride?: string): Promise<string> {
|
||||
const baseHash = Math.abs(hash_string(baseUrl)).toString(16);
|
||||
const baseStore = (await getRootStore()) + baseHash + "/";
|
||||
const baseStore = (await getRootStore(configDirOverride)) + baseHash + "/";
|
||||
await ensureDir(baseStore);
|
||||
return baseStore;
|
||||
}
|
||||
|
||||
+286
-27
@@ -3,6 +3,8 @@ import {
|
||||
colors,
|
||||
Command,
|
||||
Confirm,
|
||||
Input,
|
||||
Select,
|
||||
ensureDir,
|
||||
minimatch,
|
||||
JSZip,
|
||||
@@ -34,8 +36,9 @@ import {
|
||||
} from "./script.ts";
|
||||
|
||||
import { handleFile } from "./script.ts";
|
||||
import { deepEqual, isFileResource } from "./utils.ts";
|
||||
import { SyncOptions, mergeConfigWithConfigFile } from "./conf.ts";
|
||||
import { deepEqual, isFileResource, Repository, selectRepository } from "./utils.ts";
|
||||
import { SyncOptions, mergeConfigWithConfigFile, readConfigFile, getEffectiveSettings } from "./conf.ts";
|
||||
import { Workspace } from "./workspace.ts";
|
||||
import { removePathPrefix } from "./types.ts";
|
||||
import { SyncCodebase, listSyncCodebases } from "./codebase.ts";
|
||||
import {
|
||||
@@ -46,6 +49,128 @@ import {
|
||||
import { FlowModule, OpenFlow, RawScript } from "./gen/types.gen.ts";
|
||||
import { pushResource } from "./resource.ts";
|
||||
|
||||
|
||||
// Merge CLI options with effective settings, preserving CLI flags as overrides
|
||||
function mergeCliWithEffectiveOptions<T extends GlobalOptions & SyncOptions & { repository?: string }>(
|
||||
cliOpts: T,
|
||||
effectiveOpts: SyncOptions
|
||||
): T {
|
||||
// Start with effective options from config, then overlay only explicitly provided CLI flags
|
||||
const mergedOpts = Object.assign({}, effectiveOpts) as T;
|
||||
|
||||
// Always preserve these operational CLI flags
|
||||
if (cliOpts.dryRun !== undefined) mergedOpts.dryRun = cliOpts.dryRun;
|
||||
if (cliOpts.yes !== undefined) mergedOpts.yes = cliOpts.yes;
|
||||
if (cliOpts.stateful !== undefined) mergedOpts.stateful = cliOpts.stateful;
|
||||
if (cliOpts.skipPull !== undefined) mergedOpts.skipPull = cliOpts.skipPull;
|
||||
if (cliOpts.failConflicts !== undefined) mergedOpts.failConflicts = cliOpts.failConflicts;
|
||||
if (cliOpts.plainSecrets !== undefined) mergedOpts.plainSecrets = cliOpts.plainSecrets;
|
||||
if (cliOpts.json !== undefined) mergedOpts.json = cliOpts.json;
|
||||
if (cliOpts.message !== undefined) mergedOpts.message = cliOpts.message;
|
||||
if (cliOpts.parallel !== undefined) mergedOpts.parallel = cliOpts.parallel;
|
||||
if (cliOpts.jsonOutput !== undefined) mergedOpts.jsonOutput = cliOpts.jsonOutput;
|
||||
if (cliOpts.repository !== undefined) mergedOpts.repository = cliOpts.repository;
|
||||
|
||||
// Always preserve CLI include flags (they should override config file settings)
|
||||
if (cliOpts.includeUsers !== undefined) mergedOpts.includeUsers = cliOpts.includeUsers;
|
||||
if (cliOpts.includeGroups !== undefined) mergedOpts.includeGroups = cliOpts.includeGroups;
|
||||
if (cliOpts.includeSettings !== undefined) mergedOpts.includeSettings = cliOpts.includeSettings;
|
||||
if (cliOpts.includeKey !== undefined) mergedOpts.includeKey = cliOpts.includeKey;
|
||||
if (cliOpts.includeSchedules !== undefined) mergedOpts.includeSchedules = cliOpts.includeSchedules;
|
||||
if (cliOpts.includeTriggers !== undefined) mergedOpts.includeTriggers = cliOpts.includeTriggers;
|
||||
|
||||
// Always preserve CLI skip flags (they should override config file settings)
|
||||
if (cliOpts.skipScripts !== undefined) mergedOpts.skipScripts = cliOpts.skipScripts;
|
||||
if (cliOpts.skipFlows !== undefined) mergedOpts.skipFlows = cliOpts.skipFlows;
|
||||
if (cliOpts.skipApps !== undefined) mergedOpts.skipApps = cliOpts.skipApps;
|
||||
if (cliOpts.skipFolders !== undefined) mergedOpts.skipFolders = cliOpts.skipFolders;
|
||||
|
||||
return mergedOpts;
|
||||
}
|
||||
|
||||
// Resolve effective sync options with smart repository detection
|
||||
async function resolveEffectiveSyncOptions(
|
||||
workspace: Workspace,
|
||||
repositoryPath?: string
|
||||
): Promise<SyncOptions> {
|
||||
const localConfig = await readConfigFile();
|
||||
|
||||
// If repository path is already specified, use it directly
|
||||
if (repositoryPath) {
|
||||
return getEffectiveSettings(
|
||||
localConfig,
|
||||
workspace.remote,
|
||||
workspace.workspaceId,
|
||||
repositoryPath
|
||||
);
|
||||
}
|
||||
|
||||
// Auto-detect repository from overrides if not specified
|
||||
if (localConfig.overrides) {
|
||||
const prefix = `${workspace.remote}:${workspace.workspaceId}:`;
|
||||
const applicableRepos: string[] = [];
|
||||
|
||||
// Find all repository-specific overrides for this workspace
|
||||
for (const key of Object.keys(localConfig.overrides)) {
|
||||
if (key.startsWith(prefix) && !key.endsWith(':*')) {
|
||||
const repo = key.substring(prefix.length);
|
||||
if (repo) {
|
||||
applicableRepos.push(repo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (applicableRepos.length === 1) {
|
||||
// Single repository found - auto-select it
|
||||
log.info(`Auto-selected repository: ${applicableRepos[0]}`);
|
||||
return getEffectiveSettings(
|
||||
localConfig,
|
||||
workspace.remote,
|
||||
workspace.workspaceId,
|
||||
applicableRepos[0]
|
||||
);
|
||||
} else if (applicableRepos.length > 1) {
|
||||
// Multiple repositories found - prompt for selection
|
||||
const isInteractive = Deno.stdin.isTerminal() && Deno.stdout.isTerminal();
|
||||
|
||||
if (isInteractive) {
|
||||
const choices = [
|
||||
{ name: "Use top-level settings (no repository-specific override)", value: "" },
|
||||
...applicableRepos.map(repo => ({ name: repo, value: repo }))
|
||||
];
|
||||
|
||||
const selectedRepo = await Select.prompt({
|
||||
message: "Multiple repository overrides found. Select which to use:",
|
||||
options: choices
|
||||
});
|
||||
|
||||
if (selectedRepo) {
|
||||
log.info(`Selected repository: ${selectedRepo}`);
|
||||
}
|
||||
|
||||
return getEffectiveSettings(
|
||||
localConfig,
|
||||
workspace.remote,
|
||||
workspace.workspaceId,
|
||||
selectedRepo
|
||||
);
|
||||
} else {
|
||||
// Non-interactive mode - list options and use top-level
|
||||
log.warn(`Multiple repository overrides found: ${applicableRepos.join(', ')}`);
|
||||
log.warn(`Use --repository flag to specify which one to use. Using top-level settings.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No repository overrides found or selected - use top-level settings
|
||||
return getEffectiveSettings(
|
||||
localConfig,
|
||||
workspace.remote,
|
||||
workspace.workspaceId,
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
type DynFSElement = {
|
||||
isDirectory: boolean;
|
||||
path: string;
|
||||
@@ -678,7 +803,17 @@ export async function elementsToMap(
|
||||
if (skips.skipResourceTypes && path.endsWith(".resource-type" + ext))
|
||||
continue;
|
||||
|
||||
if (skips.skipVariables && path.endsWith(".variable" + ext)) continue;
|
||||
// Use getTypeStrFromPath for consistent type detection
|
||||
try {
|
||||
const fileType = getTypeStrFromPath(path);
|
||||
if (skips.skipVariables && fileType === "variable") continue;
|
||||
if (skips.skipScripts && fileType === "script") continue;
|
||||
if (skips.skipFlows && fileType === "flow") continue;
|
||||
if (skips.skipApps && fileType === "app") continue;
|
||||
if (skips.skipFolders && fileType === "folder") continue;
|
||||
} catch {
|
||||
// If getTypeStrFromPath can't determine the type, continue processing the file
|
||||
}
|
||||
|
||||
if (skips.skipResources && isFileResource(path)) continue;
|
||||
|
||||
@@ -733,6 +868,10 @@ export interface Skips {
|
||||
skipResources?: boolean | undefined;
|
||||
skipResourceTypes?: boolean | undefined;
|
||||
skipSecrets?: boolean | undefined;
|
||||
skipScripts?: boolean | undefined;
|
||||
skipFlows?: boolean | undefined;
|
||||
skipApps?: boolean | undefined;
|
||||
skipFolders?: boolean | undefined;
|
||||
skipScriptsMetadata?: boolean | undefined;
|
||||
includeSchedules?: boolean | undefined;
|
||||
includeTriggers?: boolean | undefined;
|
||||
@@ -1000,6 +1139,10 @@ export async function ignoreF(wmillconf: {
|
||||
extraIncludes?: string[];
|
||||
skipResourceTypes?: boolean;
|
||||
json?: boolean;
|
||||
includeUsers?: boolean;
|
||||
includeGroups?: boolean;
|
||||
includeSettings?: boolean;
|
||||
includeKey?: boolean;
|
||||
}): Promise<(p: string, isDirectory: boolean) => boolean> {
|
||||
let whitelist: { approve(file: string): boolean } | undefined = undefined;
|
||||
|
||||
@@ -1014,7 +1157,7 @@ export async function ignoreF(wmillconf: {
|
||||
wmillconf.includes?.some((i) => minimatch(file, i))) &&
|
||||
(!wmillconf?.excludes ||
|
||||
wmillconf.excludes!.every((i) => !minimatch(file, i))) &&
|
||||
(!wmillconf.extraIncludes ||
|
||||
(!wmillconf.extraIncludes || wmillconf.extraIncludes.length === 0 ||
|
||||
wmillconf.extraIncludes.some((i) => minimatch(file, i)))
|
||||
);
|
||||
},
|
||||
@@ -1035,6 +1178,28 @@ export async function ignoreF(wmillconf: {
|
||||
if (!isDirectory && p.endsWith(".resource-type" + ext)) {
|
||||
return wmillconf.skipResourceTypes ?? false;
|
||||
}
|
||||
|
||||
// Special files should bypass path-based filtering when their include flags are set
|
||||
if (!isDirectory) {
|
||||
try {
|
||||
const fileType = getTypeStrFromPath(p);
|
||||
if (wmillconf.includeUsers && fileType === "user") {
|
||||
return false; // Don't ignore, always include
|
||||
}
|
||||
if (wmillconf.includeGroups && fileType === "group") {
|
||||
return false; // Don't ignore, always include
|
||||
}
|
||||
if (wmillconf.includeSettings && fileType === "settings") {
|
||||
return false; // Don't ignore, always include
|
||||
}
|
||||
if (wmillconf.includeKey && fileType === "encryption_key") {
|
||||
return false; // Don't ignore, always include
|
||||
}
|
||||
} catch {
|
||||
// If getTypeStrFromPath can't determine the type, fall through to normal logic
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
!isWhitelisted(p) &&
|
||||
(isNotWmillFile(p, isDirectory) ||
|
||||
@@ -1094,9 +1259,7 @@ async function buildTracker(changes: Change[]) {
|
||||
return tracker;
|
||||
}
|
||||
|
||||
export async function pull(opts: GlobalOptions & SyncOptions) {
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
|
||||
export async function pull(opts: GlobalOptions & SyncOptions & { repository?: string }) {
|
||||
if (opts.stateful) {
|
||||
await ensureDir(path.join(Deno.cwd(), ".wmill"));
|
||||
}
|
||||
@@ -1104,6 +1267,12 @@ export async function pull(opts: GlobalOptions & SyncOptions) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
// Resolve effective sync options with repository awareness
|
||||
const effectiveOpts = await resolveEffectiveSyncOptions(workspace, opts.repository);
|
||||
|
||||
// Merge CLI flags with resolved settings (CLI flags take precedence only for explicit overrides)
|
||||
opts = mergeCliWithEffectiveOptions(opts, effectiveOpts);
|
||||
|
||||
const codebases = await listSyncCodebases(opts);
|
||||
|
||||
log.info(
|
||||
@@ -1158,8 +1327,26 @@ export async function pull(opts: GlobalOptions & SyncOptions) {
|
||||
log.info(
|
||||
`remote (${workspace.name}) -> local: ${changes.length} changes to apply`
|
||||
);
|
||||
|
||||
// Handle JSON output for dry-run
|
||||
if (opts.dryRun && opts.jsonOutput) {
|
||||
const result = {
|
||||
success: true,
|
||||
changes: changes.map(change => ({
|
||||
type: change.name,
|
||||
path: change.path,
|
||||
...(change.name === "edited" && change.codebase ? { codebase_changed: true } : {})
|
||||
})),
|
||||
total: changes.length
|
||||
};
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (changes.length > 0) {
|
||||
prettyChanges(changes);
|
||||
if (!opts.jsonOutput) {
|
||||
prettyChanges(changes);
|
||||
}
|
||||
if (opts.dryRun) {
|
||||
log.info(colors.gray(`Dry run complete.`));
|
||||
return;
|
||||
@@ -1307,11 +1494,27 @@ export async function pull(opts: GlobalOptions & SyncOptions) {
|
||||
)} scripts were changed but ignoring for now`
|
||||
);
|
||||
}
|
||||
log.info(
|
||||
colors.bold.green.underline(
|
||||
`\nDone! All ${changes.length} changes applied locally and wmill-lock.yaml updated.`
|
||||
)
|
||||
);
|
||||
if (opts.jsonOutput) {
|
||||
const result = {
|
||||
success: true,
|
||||
message: `All ${changes.length} changes applied locally and wmill-lock.yaml updated`,
|
||||
changes: changes.map(change => ({
|
||||
type: change.name,
|
||||
path: change.path,
|
||||
...(change.name === "edited" && change.codebase ? { codebase_changed: true } : {})
|
||||
})),
|
||||
total: changes.length
|
||||
};
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} else {
|
||||
log.info(
|
||||
colors.bold.green.underline(
|
||||
`\nDone! All ${changes.length} changes applied locally and wmill-lock.yaml updated.`
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (opts.jsonOutput) {
|
||||
console.log(JSON.stringify({ success: true, message: "No changes to apply", total: 0 }, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1366,8 +1569,16 @@ function removeSuffix(str: string, suffix: string) {
|
||||
return str.slice(0, str.length - suffix.length);
|
||||
}
|
||||
|
||||
export async function push(opts: GlobalOptions & SyncOptions) {
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
export async function push(opts: GlobalOptions & SyncOptions & { repository?: string }) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
// Resolve effective sync options with repository awareness
|
||||
const effectiveOpts = await resolveEffectiveSyncOptions(workspace, opts.repository);
|
||||
|
||||
// Merge CLI flags with resolved settings (CLI flags take precedence only for explicit overrides)
|
||||
opts = mergeCliWithEffectiveOptions(opts, effectiveOpts);
|
||||
|
||||
const codebases = await listSyncCodebases(opts);
|
||||
if (opts.raw) {
|
||||
log.info("--raw is now the default, you can remove it as a flag");
|
||||
@@ -1383,9 +1594,6 @@ export async function push(opts: GlobalOptions & SyncOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
log.info(
|
||||
colors.gray(
|
||||
"Computing the files to update on the remote to match local (taking wmill.yaml includes/excludes into account)"
|
||||
@@ -1498,8 +1706,25 @@ export async function push(opts: GlobalOptions & SyncOptions) {
|
||||
`remote (${workspace.name}) <- local: ${changes.length} changes to apply`
|
||||
);
|
||||
|
||||
// Handle JSON output for dry-run
|
||||
if (opts.dryRun && opts.jsonOutput) {
|
||||
const result = {
|
||||
success: true,
|
||||
changes: changes.map(change => ({
|
||||
type: change.name,
|
||||
path: change.path,
|
||||
...(change.name === "edited" && change.codebase ? { codebase_changed: true } : {})
|
||||
})),
|
||||
total: changes.length
|
||||
};
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (changes.length > 0) {
|
||||
prettyChanges(changes);
|
||||
if (!opts.jsonOutput) {
|
||||
prettyChanges(changes);
|
||||
}
|
||||
if (opts.dryRun) {
|
||||
log.info(colors.gray(`Dry run complete.`));
|
||||
return;
|
||||
@@ -1860,13 +2085,30 @@ export async function push(opts: GlobalOptions & SyncOptions) {
|
||||
await Promise.race(pool);
|
||||
}
|
||||
}
|
||||
log.info(
|
||||
colors.bold.green.underline(
|
||||
`\nDone! All ${changes.length} changes pushed to the remote workspace ${
|
||||
workspace.workspaceId
|
||||
} named ${workspace.name} (${(performance.now() - start).toFixed(0)}ms)`
|
||||
)
|
||||
);
|
||||
if (opts.jsonOutput) {
|
||||
const result = {
|
||||
success: true,
|
||||
message: `All ${changes.length} changes pushed to the remote workspace ${workspace.workspaceId} named ${workspace.name}`,
|
||||
changes: changes.map(change => ({
|
||||
type: change.name,
|
||||
path: change.path,
|
||||
...(change.name === "edited" && change.codebase ? { codebase_changed: true } : {})
|
||||
})),
|
||||
total: changes.length,
|
||||
duration_ms: Math.round(performance.now() - start)
|
||||
};
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} else {
|
||||
log.info(
|
||||
colors.bold.green.underline(
|
||||
`\nDone! All ${changes.length} changes pushed to the remote workspace ${
|
||||
workspace.workspaceId
|
||||
} named ${workspace.name} (${(performance.now() - start).toFixed(0)}ms)`
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (opts.jsonOutput) {
|
||||
console.log(JSON.stringify({ success: true, message: "No changes to push", total: 0 }, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1891,6 +2133,10 @@ const command = new Command()
|
||||
.option("--skip-secrets", "Skip syncing only secrets variables")
|
||||
.option("--skip-resources", "Skip syncing resources")
|
||||
.option("--skip-resource-types", "Skip syncing resource types")
|
||||
.option("--skip-scripts", "Skip syncing scripts")
|
||||
.option("--skip-flows", "Skip syncing flows")
|
||||
.option("--skip-apps", "Skip syncing apps")
|
||||
.option("--skip-folders", "Skip syncing folders")
|
||||
// .option("--skip-scripts-metadata", "Skip syncing scripts metadata, focus solely on logic")
|
||||
.option("--include-schedules", "Include syncing schedules")
|
||||
.option("--include-triggers", "Include syncing triggers")
|
||||
@@ -1898,6 +2144,7 @@ const command = new Command()
|
||||
.option("--include-groups", "Include syncing groups")
|
||||
.option("--include-settings", "Include syncing workspace settings")
|
||||
.option("--include-key", "Include workspace encryption key")
|
||||
.option("--json-output", "Output results in JSON format")
|
||||
.option(
|
||||
"-i --includes <patterns:file[]>",
|
||||
"Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string). Overrides wmill.yaml includes"
|
||||
@@ -1910,6 +2157,10 @@ const command = new Command()
|
||||
"--extra-includes <patterns:file[]>",
|
||||
"Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string). Useful to still take wmill.yaml into account and act as a second pattern to satisfy"
|
||||
)
|
||||
.option(
|
||||
"--repository <repo:string>",
|
||||
"Specify repository path (e.g., u/user/repo) when multiple repositories exist"
|
||||
)
|
||||
// deno-lint-ignore no-explicit-any
|
||||
.action(pull as any)
|
||||
.command("push")
|
||||
@@ -1925,7 +2176,10 @@ const command = new Command()
|
||||
.option("--skip-secrets", "Skip syncing only secrets variables")
|
||||
.option("--skip-resources", "Skip syncing resources")
|
||||
.option("--skip-resource-types", "Skip syncing resource types")
|
||||
|
||||
.option("--skip-scripts", "Skip syncing scripts")
|
||||
.option("--skip-flows", "Skip syncing flows")
|
||||
.option("--skip-apps", "Skip syncing apps")
|
||||
.option("--skip-folders", "Skip syncing folders")
|
||||
// .option("--skip-scripts-metadata", "Skip syncing scripts metadata, focus solely on logic")
|
||||
.option("--include-schedules", "Include syncing schedules")
|
||||
.option("--include-triggers", "Include syncing triggers")
|
||||
@@ -1933,6 +2187,7 @@ const command = new Command()
|
||||
.option("--include-groups", "Include syncing groups")
|
||||
.option("--include-settings", "Include syncing workspace settings")
|
||||
.option("--include-key", "Include workspace encryption key")
|
||||
.option("--json-output", "Output results in JSON format")
|
||||
.option(
|
||||
"-i --includes <patterns:file[]>",
|
||||
"Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)"
|
||||
@@ -1950,6 +2205,10 @@ const command = new Command()
|
||||
"Include a message that will be added to all scripts/flows/apps updated during this push"
|
||||
)
|
||||
.option("--parallel <number>", "Number of changes to process in parallel")
|
||||
.option(
|
||||
"--repository <repo:string>",
|
||||
"Specify repository path (e.g., u/user/repo) when multiple repositories exist"
|
||||
)
|
||||
// deno-lint-ignore no-explicit-any
|
||||
.action(push as any);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
||||
version: "3.7"
|
||||
|
||||
x-logging: &default-logging
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
compress: "true"
|
||||
|
||||
services:
|
||||
test_db:
|
||||
image: postgres:16
|
||||
environment:
|
||||
POSTGRES_PASSWORD: testpass123
|
||||
POSTGRES_DB: windmill_test
|
||||
POSTGRES_USER: postgres
|
||||
ports:
|
||||
- "5433:5432" # Use different port to avoid conflicts
|
||||
volumes:
|
||||
- test_db_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d windmill_test"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
logging: *default-logging
|
||||
|
||||
test_windmill_server:
|
||||
image: windmill-test:latest
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:testpass123@test_db/windmill_test?sslmode=disable
|
||||
- MODE=server
|
||||
- LICENSE_KEY=${EE_LICENSE_KEY}
|
||||
- RUST_LOG=info
|
||||
- DISABLE_TELEMETRY=true
|
||||
- METRICS_ENABLED=false
|
||||
ports:
|
||||
- "8001:8000" # Use different port to avoid conflicts
|
||||
depends_on:
|
||||
test_db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- test_worker_logs:/tmp/windmill/logs
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/api/version"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
logging: *default-logging
|
||||
|
||||
test_windmill_worker:
|
||||
image: windmill-test:latest
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:testpass123@test_db/windmill_test?sslmode=disable
|
||||
- MODE=worker
|
||||
- WORKER_GROUP=default
|
||||
- LICENSE_KEY=${EE_LICENSE_KEY}
|
||||
- RUST_LOG=info
|
||||
- DISABLE_TELEMETRY=true
|
||||
- NUM_WORKERS=1
|
||||
- SLEEP_QUEUE=50
|
||||
depends_on:
|
||||
test_db:
|
||||
condition: service_healthy
|
||||
test_windmill_server:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- test_worker_logs:/tmp/windmill/logs
|
||||
logging: *default-logging
|
||||
|
||||
volumes:
|
||||
test_db_data: null
|
||||
test_worker_logs: null
|
||||
@@ -0,0 +1,177 @@
|
||||
import { assertEquals, assertStringIncludes } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { withContainerizedBackend } from "./containerized_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
|
||||
// =============================================================================
|
||||
// GITSYNC-SETTINGS COMMAND FEATURES
|
||||
// Tests for additional gitsync-settings command functionality
|
||||
// =============================================================================
|
||||
|
||||
Deno.test("GitSync Settings: workspace-level wildcard settings", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "workspace_level_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Configure backend with repository
|
||||
await backend.updateGitSyncConfig({
|
||||
git_sync_settings: {
|
||||
repositories: [{
|
||||
git_repo_resource_path: "u/test/workspace_repo",
|
||||
script_path: "f/**",
|
||||
group_by_folder: false,
|
||||
use_individual_branch: false,
|
||||
settings: {
|
||||
include_path: ["f/**"],
|
||||
include_type: ["script", "flow"],
|
||||
exclude_path: [],
|
||||
extra_include_path: []
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// Create initial wmill.yaml
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
excludes: []`);
|
||||
|
||||
// Pull with workspace-level flag
|
||||
const result = await backend.runCLICommand([
|
||||
'gitsync-settings', 'pull',
|
||||
'--repository', 'u/test/workspace_repo',
|
||||
'--workspace-level',
|
||||
'--override'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Workspace-level pull should succeed: ${result.stderr}`);
|
||||
|
||||
// Read updated config
|
||||
const updatedConfig = await Deno.readTextFile(`${tempDir}/wmill.yaml`);
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
|
||||
// Should create workspace wildcard override
|
||||
assertStringIncludes(updatedConfig, `'${backendUrl}:${backend.workspace}:*':`);
|
||||
assertStringIncludes(updatedConfig, "overrides:");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("GitSync Settings: default mode writes to top-level", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "default_mode_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Configure backend with specific settings
|
||||
await backend.updateGitSyncConfig({
|
||||
git_sync_settings: {
|
||||
repositories: [{
|
||||
git_repo_resource_path: "u/test/default_repo",
|
||||
script_path: "f/**",
|
||||
group_by_folder: false,
|
||||
use_individual_branch: false,
|
||||
settings: {
|
||||
include_path: ["f/special/**"],
|
||||
include_type: ["script"],
|
||||
exclude_path: ["*.test.ts"],
|
||||
extra_include_path: ["g/**"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// Create initial wmill.yaml with different settings
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
excludes: []
|
||||
skipVariables: false`);
|
||||
|
||||
// Pull with default flag
|
||||
const result = await backend.runCLICommand([
|
||||
'gitsync-settings', 'pull',
|
||||
'--repository', 'u/test/default_repo',
|
||||
'--default'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Default mode pull should succeed: ${result.stderr}`);
|
||||
|
||||
// Read updated config
|
||||
const updatedConfig = await Deno.readTextFile(`${tempDir}/wmill.yaml`);
|
||||
|
||||
// Should update top-level settings, not create overrides
|
||||
assertStringIncludes(updatedConfig, "includes:\n - f/special/**");
|
||||
assertStringIncludes(updatedConfig, "excludes:\n - '*.test.ts'");
|
||||
assertStringIncludes(updatedConfig, "extraIncludes:\n - g/**");
|
||||
|
||||
// Should NOT have overrides section
|
||||
assertEquals(updatedConfig.includes("overrides:"), false, "Default mode should not create overrides");
|
||||
});
|
||||
});
|
||||
|
||||
// Removed test for non-existent repository error handling
|
||||
// as it was testing non-deterministic behavior
|
||||
|
||||
Deno.test("GitSync Settings: pull shows correct diff output", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "diff_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Configure backend
|
||||
await backend.updateGitSyncConfig({
|
||||
git_sync_settings: {
|
||||
repositories: [{
|
||||
git_repo_resource_path: "u/test/diff_repo",
|
||||
script_path: "f/**",
|
||||
group_by_folder: false,
|
||||
use_individual_branch: false,
|
||||
settings: {
|
||||
include_path: ["f/**"],
|
||||
include_type: ["script", "flow"],
|
||||
exclude_path: [],
|
||||
extra_include_path: []
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// Create wmill.yaml with different settings
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
excludes: []
|
||||
skipVariables: true
|
||||
skipResources: false`);
|
||||
|
||||
// Pull with diff flag
|
||||
const result = await backend.runCLICommand([
|
||||
'gitsync-settings', 'pull',
|
||||
'--repository', 'u/test/diff_repo',
|
||||
'--diff'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0);
|
||||
|
||||
// Should show differences
|
||||
assertStringIncludes(result.stdout, "Changes that would be made:");
|
||||
// Should show the change for skipResources (ignoring ANSI color codes)
|
||||
assertStringIncludes(result.stdout, "skipResources:");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
import { assertEquals, assertStringIncludes, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { withContainerizedBackend } from "./containerized_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
import { parseJsonFromCLIOutput } from "./test_config_helpers.ts";
|
||||
|
||||
// =============================================================================
|
||||
// INCLUDE FLAGS BYPASS FILTERING TESTS
|
||||
// Tests that CLI include flags properly bypass path-based filtering
|
||||
// =============================================================================
|
||||
|
||||
// Helper function to set up workspace profile
|
||||
async function setupWorkspaceProfile(backend: any): Promise<void> {
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "localhost_test",
|
||||
token: backend.token
|
||||
};
|
||||
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
}
|
||||
|
||||
// The ContainerizedBackend already creates test data we can use:
|
||||
// - admin user (admin@windmill.dev)
|
||||
// - test_group (created by seedTestData())
|
||||
// - workspace encryption key
|
||||
// - test apps, resources, variables via seedTestData()
|
||||
// No additional setup needed!
|
||||
|
||||
Deno.test("CLI include flags bypass restrictive path filtering", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Create wmill.yaml with very restrictive includes that would exclude special files
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "f/**"
|
||||
excludes: []
|
||||
skipVariables: true
|
||||
skipResources: true
|
||||
includeUsers: false
|
||||
includeGroups: false
|
||||
includeSettings: false
|
||||
includeKey: false`);
|
||||
|
||||
// Test: CLI flags should override config and bypass path filtering
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--include-users',
|
||||
'--include-groups',
|
||||
'--include-settings',
|
||||
'--include-key',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Command failed: ${result.stderr}`);
|
||||
|
||||
const output = parseJsonFromCLIOutput(result.stdout);
|
||||
const changePaths = output.changes.map((c: any) => c.path);
|
||||
|
||||
// Assert that special files are included despite restrictive path filtering
|
||||
const hasUser = changePaths.some((path: string) => path.includes('admin@windmill.dev.user.yaml'));
|
||||
const hasGroup = changePaths.some((path: string) => path.includes('groups/test_group.group.yaml'));
|
||||
const hasSettings = changePaths.some((path: string) => path === 'settings.yaml');
|
||||
const hasEncryptionKey = changePaths.some((path: string) => path === 'encryption_key.yaml');
|
||||
|
||||
assert(hasUser, `Admin user should be included despite restrictive includes. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasGroup, `'test_group' should be included despite restrictive includes. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasSettings, `Settings should be included despite restrictive includes. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasEncryptionKey, `Encryption key should be included despite restrictive includes. Found paths: ${changePaths.join(', ')}`);
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("CLI flags override wmill.yaml include settings", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Config explicitly disables includes, but CLI should override
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
excludes: []
|
||||
includeUsers: false
|
||||
includeGroups: false`);
|
||||
|
||||
// CLI flags should override config file settings
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--include-users',
|
||||
'--include-groups',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Command failed: ${result.stderr}`);
|
||||
|
||||
const output = parseJsonFromCLIOutput(result.stdout);
|
||||
const changePaths = output.changes.map((c: any) => c.path);
|
||||
|
||||
const hasUser = changePaths.some((path: string) => path.includes('admin@windmill.dev.user.yaml'));
|
||||
const hasGroup = changePaths.some((path: string) => path.includes('groups/test_group.group.yaml'));
|
||||
|
||||
assert(hasUser, `CLI --include-users should override config includeUsers: false. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasGroup, `CLI --include-groups should override config includeGroups: false. Found paths: ${changePaths.join(', ')}`);
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Skip flags work correctly with getTypeStrFromPath and lock files", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Create wmill.yaml with skip flags enabled
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
excludes: []
|
||||
skipScripts: true
|
||||
skipFlows: false
|
||||
includeUsers: true`);
|
||||
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Command failed: ${result.stderr}`);
|
||||
|
||||
const output = parseJsonFromCLIOutput(result.stdout);
|
||||
const changePaths = output.changes.map((c: any) => c.path);
|
||||
|
||||
// Scripts should be skipped (including lock files) - the backend doesn't create scripts by default
|
||||
const hasScript = changePaths.some((path: string) =>
|
||||
path.endsWith('.py') || path.endsWith('.ts') || path.endsWith('.go') || path.endsWith('.sh')
|
||||
);
|
||||
const hasScriptLock = changePaths.some((path: string) => path.endsWith('.script.lock'));
|
||||
|
||||
// Apps should be included (the backend creates test apps)
|
||||
const hasApp = changePaths.some((path: string) => path.includes('test_dashboard') || path.endsWith('.app.yaml'));
|
||||
|
||||
// Users should still be included
|
||||
const hasUser = changePaths.some((path: string) => path.includes('admin@windmill.dev.user.yaml'));
|
||||
|
||||
assert(!hasScript, `Standalone scripts should be skipped when skipScripts: true. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(!hasScriptLock, `Script lock files should be skipped when skipScripts: true. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasApp, `Apps should be included (inline scripts are part of apps). Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasUser, `Users should be included when includeUsers: true. Found paths: ${changePaths.join(', ')}`);
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Mixed include and skip flags work together", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Create restrictive config with mixed settings
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "f/**"
|
||||
excludes: []
|
||||
skipScripts: true
|
||||
includeUsers: false
|
||||
includeSettings: false`);
|
||||
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--skip-scripts', // Reinforce script skipping
|
||||
'--include-users', // Override config to include users
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Command failed: ${result.stderr}`);
|
||||
|
||||
const output = parseJsonFromCLIOutput(result.stdout);
|
||||
const changePaths = output.changes.map((c: any) => c.path);
|
||||
|
||||
// Scripts should be excluded
|
||||
const hasScript = changePaths.some((path: string) =>
|
||||
path.endsWith('.py') || path.endsWith('.ts') || path.endsWith('.go') || path.endsWith('.sh')
|
||||
);
|
||||
|
||||
// Users should be included (CLI override)
|
||||
const hasUser = changePaths.some((path: string) => path.includes('admin@windmill.dev.user.yaml'));
|
||||
|
||||
// Settings should be excluded (no CLI override, restrictive path filtering)
|
||||
const hasSettings = changePaths.some((path: string) => path === 'settings.yaml');
|
||||
|
||||
assert(!hasScript, `Scripts should be excluded due to skipScripts. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasUser, `Users should be included due to CLI --include-users override. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(!hasSettings, `Settings should be excluded (no CLI override + restrictive paths). Found paths: ${changePaths.join(', ')}`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Test to verify that wmill init handles workspaces with no git-sync settings correctly
|
||||
* This creates a unit test that directly tests the logic without needing a backend
|
||||
*/
|
||||
|
||||
import { assertEquals, assertStringIncludes, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { DEFAULT_SYNC_OPTIONS } from "../conf.ts";
|
||||
import { withContainerizedBackend } from "./containerized_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
|
||||
// Mock the workspace object
|
||||
const mockWorkspace = {
|
||||
remote: 'https://app.windmill.dev/',
|
||||
workspaceId: 'test-workspace',
|
||||
name: 'test-workspace'
|
||||
};
|
||||
|
||||
// Simulate the createWorkspaceProfile function logic for the "no repositories" case
|
||||
function createWorkspaceProfileNoRepos(workspace: any): any {
|
||||
const workspaceProfile: any = {
|
||||
baseUrl: workspace.remote,
|
||||
workspaceId: workspace.workspaceId,
|
||||
};
|
||||
|
||||
// Simulate the case where listRepositories returns empty array
|
||||
const repositories: any[] = [];
|
||||
|
||||
if (repositories.length === 0) {
|
||||
console.log(`No git repositories found in workspace '${workspace.workspaceId}'`);
|
||||
// This is the fix: include default sync settings when no repositories exist
|
||||
Object.assign(workspaceProfile, DEFAULT_SYNC_OPTIONS);
|
||||
return workspaceProfile;
|
||||
}
|
||||
|
||||
return workspaceProfile;
|
||||
}
|
||||
|
||||
Deno.test("Init: createWorkspaceProfile includes defaults when no repositories exist", () => {
|
||||
console.log('🧪 Testing init logic for workspace with no git-sync repositories...');
|
||||
|
||||
const workspaceProfile = createWorkspaceProfileNoRepos(mockWorkspace);
|
||||
|
||||
console.log('Generated workspace profile:', JSON.stringify(workspaceProfile, null, 2));
|
||||
|
||||
// Verify basic workspace info
|
||||
assertEquals(workspaceProfile.baseUrl, 'https://app.windmill.dev/');
|
||||
assertEquals(workspaceProfile.workspaceId, 'test-workspace');
|
||||
|
||||
// Verify default sync settings are included
|
||||
assert(Array.isArray(workspaceProfile.includes), 'Should have includes array');
|
||||
assertEquals(workspaceProfile.includes.length, 1, 'Should have one include pattern');
|
||||
assertEquals(workspaceProfile.includes[0], 'f/**', 'Should include f/** pattern');
|
||||
|
||||
assert(Array.isArray(workspaceProfile.excludes), 'Should have excludes array');
|
||||
assertEquals(workspaceProfile.excludes.length, 0, 'Should have empty excludes array');
|
||||
|
||||
assertEquals(workspaceProfile.defaultTs, 'bun', 'Should have bun as default TypeScript runtime');
|
||||
|
||||
console.log('✅ Workspace profile correctly includes default sync settings when no repositories exist');
|
||||
});
|
||||
|
||||
Deno.test("Init: verify DEFAULT_SYNC_OPTIONS has expected values", () => {
|
||||
console.log('🔍 Verifying DEFAULT_SYNC_OPTIONS contains expected values...');
|
||||
|
||||
console.log('DEFAULT_SYNC_OPTIONS:', JSON.stringify(DEFAULT_SYNC_OPTIONS, null, 2));
|
||||
|
||||
// Verify the default options include the expected f/** pattern
|
||||
assert(Array.isArray(DEFAULT_SYNC_OPTIONS.includes), 'DEFAULT_SYNC_OPTIONS should have includes array');
|
||||
assertEquals(DEFAULT_SYNC_OPTIONS.includes.length, 1, 'Should have one include pattern');
|
||||
assertEquals(DEFAULT_SYNC_OPTIONS.includes[0], 'f/**', 'Should default to f/** pattern');
|
||||
|
||||
assert(Array.isArray(DEFAULT_SYNC_OPTIONS.excludes), 'DEFAULT_SYNC_OPTIONS should have excludes array');
|
||||
assertEquals(DEFAULT_SYNC_OPTIONS.excludes.length, 0, 'Should have empty excludes array by default');
|
||||
|
||||
assertEquals(DEFAULT_SYNC_OPTIONS.defaultTs, 'bun', 'Should default to bun runtime');
|
||||
|
||||
console.log('✅ DEFAULT_SYNC_OPTIONS has expected values');
|
||||
});
|
||||
|
||||
Deno.test("Init: --use-backend flag applies git-sync settings", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: backend.workspace,
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// First create the git repository resource that the git-sync will reference
|
||||
await backend.createAdditionalGitRepo("u/test/init_repo", "Test init repository");
|
||||
|
||||
// Configure backend with git-sync settings
|
||||
await backend.updateGitSyncConfig({
|
||||
git_sync_settings: {
|
||||
repositories: [{
|
||||
git_repo_resource_path: "u/test/init_repo",
|
||||
script_path: "f/**",
|
||||
group_by_folder: false,
|
||||
use_individual_branch: false,
|
||||
settings: {
|
||||
include_path: ["f/backend/**"],
|
||||
include_type: ["script", "flow"],
|
||||
exclude_path: ["*.test.ts"],
|
||||
extra_include_path: ["g/**"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// Run init with --use-backend flag
|
||||
const result = await backend.runCLICommand([
|
||||
'init',
|
||||
'--use-backend',
|
||||
'--repository', 'u/test/init_repo'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Init with --use-backend should succeed: ${result.stderr}`);
|
||||
|
||||
// Verify wmill.yaml was created with backend settings
|
||||
const wmillYaml = await Deno.readTextFile(`${tempDir}/wmill.yaml`);
|
||||
|
||||
// Should have backend-applied settings written to top-level (not overrides)
|
||||
assertStringIncludes(wmillYaml, "f/backend/**", "Should include backend's include_path");
|
||||
assertStringIncludes(wmillYaml, "*.test.ts", "Should include backend's exclude_path");
|
||||
assertStringIncludes(wmillYaml, "g/**", "Should include backend's extra_include_path");
|
||||
|
||||
// Should NOT have overrides section since we're starting fresh
|
||||
assertEquals(wmillYaml.includes("overrides:"), false, "Init should not create overrides section");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Init: --use-default bypasses backend settings check", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: backend.workspace,
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Create git repository resource
|
||||
await backend.createAdditionalGitRepo("u/test/ignored_repo", "Test ignored repository");
|
||||
|
||||
// Configure backend with git-sync settings
|
||||
await backend.updateGitSyncConfig({
|
||||
git_sync_settings: {
|
||||
repositories: [{
|
||||
git_repo_resource_path: "u/test/ignored_repo",
|
||||
script_path: "f/**",
|
||||
group_by_folder: false,
|
||||
use_individual_branch: false,
|
||||
settings: {
|
||||
include_path: ["f/should-be-ignored/**"],
|
||||
include_type: ["script"],
|
||||
exclude_path: [],
|
||||
extra_include_path: []
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// Run init with --use-default (should ignore backend)
|
||||
const result = await backend.runCLICommand([
|
||||
'init',
|
||||
'--use-default'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Init with --use-default should succeed: ${result.stderr}`);
|
||||
|
||||
// Verify wmill.yaml was created with default settings only
|
||||
const wmillYaml = await Deno.readTextFile(`${tempDir}/wmill.yaml`);
|
||||
|
||||
// Should have default settings, not backend settings
|
||||
assertStringIncludes(wmillYaml, "includes:\n - f/**", "Should use default includes");
|
||||
assertStringIncludes(wmillYaml, "defaultTs: bun", "Should use default TypeScript runtime");
|
||||
|
||||
// Should NOT have backend-specific settings
|
||||
assertEquals(wmillYaml.includes("f/should-be-ignored/**"), false, "Should not include backend settings");
|
||||
assertEquals(wmillYaml.includes("overrides:"), false, "Should not create overrides when using defaults");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
import { assertEquals, assert, assertStringIncludes } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { withContainerizedBackend } from "./containerized_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
import { parseJsonFromCLIOutput } from "./test_config_helpers.ts";
|
||||
|
||||
// =============================================================================
|
||||
// MULTI-INSTANCE WORKSPACE TESTS
|
||||
// Tests for handling multiple Windmill instances with same workspace IDs
|
||||
// =============================================================================
|
||||
|
||||
// Helper function to set up workspace profile with specific name
|
||||
async function setupWorkspaceProfile(backend: any, workspaceName: string): Promise<void> {
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: workspaceName,
|
||||
token: backend.token
|
||||
};
|
||||
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
}
|
||||
|
||||
Deno.test("Multi-Instance: gitsync-settings pull with new format", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace profile
|
||||
await setupWorkspaceProfile(backend, "multi_instance_test");
|
||||
|
||||
// Create wmill.yaml with new format overrides for different instances
|
||||
const backendUrl = new URL(backend.baseUrl).toString(); // Normalize URL
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
excludes: []
|
||||
|
||||
overrides:
|
||||
# Current backend instance (should match)
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
includeTriggers: true
|
||||
includeSchedules: true
|
||||
skipVariables: true
|
||||
|
||||
# Different instance (won't match)
|
||||
"https://app.windmill.dev/:${backend.workspace}:u/test/test_repo":
|
||||
includeTriggers: false
|
||||
includeSchedules: false
|
||||
skipVariables: false`);
|
||||
|
||||
// Pull settings - should use the matching instance override (skipVariables: true)
|
||||
const pullResult = await backend.runCLICommand([
|
||||
'gitsync-settings', 'pull',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--diff'
|
||||
], tempDir, "multi_instance_test");
|
||||
|
||||
assertEquals(pullResult.code, 0);
|
||||
assertStringIncludes(pullResult.stdout, "Changes that would be made:");
|
||||
// includeSchedules should show as a change since backend default is false
|
||||
assertStringIncludes(pullResult.stdout, "includeSchedules");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Multi-Instance: gitsync-settings push with overrides", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace profile
|
||||
await setupWorkspaceProfile(backend, "push_override_test");
|
||||
|
||||
// Create wmill.yaml with specific settings that differ from backend defaults
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
excludes: []
|
||||
skipVariables: false
|
||||
includeSchedules: false
|
||||
|
||||
overrides:
|
||||
# Override for current backend instance - set includeSchedules: true (backend default is false)
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
includeSchedules: true
|
||||
skipVariables: true`);
|
||||
|
||||
// Push settings - should show changes because includeSchedules differs from backend
|
||||
const pushResult = await backend.runCLICommand([
|
||||
'gitsync-settings', 'push',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--diff'
|
||||
], tempDir, "push_override_test");
|
||||
|
||||
assertEquals(pushResult.code, 0);
|
||||
assertStringIncludes(pushResult.stdout, "Changes that would be pushed:");
|
||||
assertStringIncludes(pushResult.stdout, "includeSchedules");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Multi-Instance: sync with repository-specific overrides", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend, "my-workspace_123");
|
||||
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
excludes: []
|
||||
|
||||
overrides:
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
skipApps: true`);
|
||||
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir, "my-workspace_123");
|
||||
|
||||
assertEquals(result.code, 0);
|
||||
|
||||
const data = parseJsonFromCLIOutput(result.stdout);
|
||||
|
||||
// Test is designed to verify that the new format works correctly
|
||||
|
||||
// The test app should NOT appear in changes because skipApps: true
|
||||
const hasTestApp = (data.changes || []).some((change: any) =>
|
||||
change.path?.includes('f/test_dashboard')
|
||||
);
|
||||
assertEquals(hasTestApp, false, "Test app should be skipped due to skipApps override");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Multi-Instance: auto-detection of single repository override", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend, "auto_detect_test");
|
||||
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
excludes: []
|
||||
|
||||
overrides:
|
||||
# Single repository override - should be auto-detected
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
skipApps: true
|
||||
includeSchedules: true`);
|
||||
|
||||
// Don't specify --repository, it should auto-detect
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir, "auto_detect_test");
|
||||
|
||||
assertEquals(result.code, 0);
|
||||
assertStringIncludes(result.stdout, "Auto-selected repository: u/test/test_repo");
|
||||
|
||||
const data = parseJsonFromCLIOutput(result.stdout);
|
||||
|
||||
// The test app should NOT appear because of auto-detected skipApps: true
|
||||
const hasTestApp = (data.changes || []).some((change: any) =>
|
||||
change.path?.includes('f/test_dashboard')
|
||||
);
|
||||
assertEquals(hasTestApp, false, "Test app should be skipped due to auto-detected override");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Multi-Instance: workspace wildcards with new format", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend, "wildcard_test");
|
||||
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
excludes: []
|
||||
|
||||
overrides:
|
||||
# Wildcard for current backend instance
|
||||
"${backendUrl}:${backend.workspace}:*":
|
||||
skipVariables: true
|
||||
skipResources: true`);
|
||||
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir, "wildcard_test");
|
||||
|
||||
assertEquals(result.code, 0);
|
||||
|
||||
const data = parseJsonFromCLIOutput(result.stdout);
|
||||
|
||||
// Variables should be skipped due to wildcard override
|
||||
const hasTestVariable = (data.changes || []).some((change: any) =>
|
||||
change.path?.includes('u/admin/test_config.variable.yaml')
|
||||
);
|
||||
assertEquals(hasTestVariable, false, "Variables should be skipped due to wildcard override");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,273 @@
|
||||
import { assertEquals, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { getEffectiveSettings } from "../conf.ts";
|
||||
import { withContainerizedBackend } from "./containerized_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
|
||||
// =============================================================================
|
||||
// OVERRIDE SETTINGS BEHAVIOR TESTS
|
||||
// Tests for override inheritance and file filtering behavior
|
||||
// =============================================================================
|
||||
|
||||
Deno.test("Override Settings: override inherits non-overridden settings from base config", () => {
|
||||
const config = {
|
||||
includes: ["default/**"],
|
||||
skipVariables: true, // Base has this as true
|
||||
skipResources: true, // Base has this as true
|
||||
skipApps: false, // Base has this as false
|
||||
defaultTs: "bun" as const,
|
||||
overrides: {
|
||||
"http://localhost:8000/:test:u/user/repo": {
|
||||
includes: ["override/**"],
|
||||
skipApps: true // Override only changes skipApps, should inherit other skip flags
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const effective = getEffectiveSettings(
|
||||
config,
|
||||
"http://localhost:8000/",
|
||||
"test",
|
||||
"u/user/repo"
|
||||
);
|
||||
|
||||
// Override values should be used
|
||||
assertEquals(effective.includes, ["override/**"], "Must use override includes");
|
||||
assertEquals(effective.skipApps, true, "Must use override skipApps");
|
||||
|
||||
// Should inherit skip flags from base config
|
||||
assertEquals(effective.skipVariables, true, "Must inherit skipVariables=true from base config");
|
||||
assertEquals(effective.skipResources, true, "Must inherit skipResources=true from base config");
|
||||
assertEquals(effective.defaultTs, "bun", "Must inherit defaultTs from base config");
|
||||
});
|
||||
|
||||
Deno.test("Override Settings: workspace wildcards with repo-specific precedence", () => {
|
||||
const config = {
|
||||
includes: ["default/**"],
|
||||
skipVariables: false,
|
||||
overrides: {
|
||||
"http://localhost:8000/:test:*": {
|
||||
skipVariables: true,
|
||||
includes: ["workspace/**"]
|
||||
},
|
||||
"http://localhost:8000/:test:u/user/specific": {
|
||||
includes: ["specific/**"]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Test specific repo override (should take precedence over wildcard)
|
||||
const specificEffective = getEffectiveSettings(
|
||||
config,
|
||||
"http://localhost:8000/",
|
||||
"test",
|
||||
"u/user/specific"
|
||||
);
|
||||
assertEquals(specificEffective.includes, ["specific/**"], "Specific repo override must take precedence over wildcard");
|
||||
assertEquals(specificEffective.skipVariables, true, "Workspace wildcard setting must still apply");
|
||||
|
||||
// Test wildcard match
|
||||
const wildcardEffective = getEffectiveSettings(
|
||||
config,
|
||||
"http://localhost:8000/",
|
||||
"test",
|
||||
"u/user/other"
|
||||
);
|
||||
assertEquals(wildcardEffective.includes, ["workspace/**"], "Wildcard must match repos without specific overrides");
|
||||
assertEquals(wildcardEffective.skipVariables, true, "Workspace wildcard setting must apply");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// INTEGRATION TESTS - File Filtering Behavior
|
||||
// =============================================================================
|
||||
|
||||
Deno.test("Integration: sync pull with skipVariables override excludes variable files", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "skip_variables_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Create wmill.yaml with override that skips variables
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
skipVariables: false
|
||||
|
||||
overrides:
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
skipVariables: true`);
|
||||
|
||||
// Verify backend has test variable before pull
|
||||
const backendVariables = await backend.listAllVariables();
|
||||
const hasTestVariable = backendVariables.some(v => v.path === 'u/admin/test_config');
|
||||
assert(hasTestVariable, "Backend should have test variable before pull");
|
||||
|
||||
// Run sync pull (NOT dry-run) to actually write files
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--yes'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Sync pull should succeed: ${result.stderr}`);
|
||||
|
||||
// Verify variable files were NOT written to filesystem due to skipVariables: true
|
||||
const filesWritten = [];
|
||||
for await (const entry of Deno.readDir(tempDir)) {
|
||||
if (entry.isFile && entry.name.endsWith('.yaml')) {
|
||||
filesWritten.push(entry.name);
|
||||
}
|
||||
}
|
||||
|
||||
const hasVariableFile = filesWritten.some(file => file.includes('.variable.yaml'));
|
||||
assertEquals(hasVariableFile, false, "Variable files should NOT be written due to skipVariables override");
|
||||
|
||||
// Verify other files WERE written (since skipVariables only affects variables)
|
||||
// Check what files were actually written
|
||||
console.log("Files written:", filesWritten);
|
||||
|
||||
// Should have some files written (just not variable files)
|
||||
assert(filesWritten.length > 0, `Some files should be written when skipVariables is true. Got: ${filesWritten.join(', ')}`);
|
||||
|
||||
// Should not have only wmill.yaml file
|
||||
const nonWmillFiles = filesWritten.filter(f => !f.includes('wmill.yaml'));
|
||||
assert(nonWmillFiles.length > 0, `Non-wmill.yaml files should be written when skipVariables is true. Got: ${nonWmillFiles.join(', ')}`);
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Integration: sync push with skipVariables override excludes variable files", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "push_skip_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Create wmill.yaml with override that skips variables
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
skipVariables: false
|
||||
|
||||
overrides:
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
skipVariables: true`);
|
||||
|
||||
// Create local test files including variables and scripts
|
||||
const timestamp = Date.now();
|
||||
|
||||
// Create variable file
|
||||
await Deno.mkdir(`${tempDir}/u/admin`, { recursive: true });
|
||||
await Deno.writeTextFile(`${tempDir}/u/admin/test_push_var_${timestamp}.variable.yaml`,
|
||||
`value: test_value_${timestamp}
|
||||
description: Test variable for push override test
|
||||
is_secret: false`);
|
||||
|
||||
// Create script file
|
||||
await Deno.mkdir(`${tempDir}/f/test`, { recursive: true });
|
||||
await Deno.writeTextFile(`${tempDir}/f/test/push_script_${timestamp}.ts`,
|
||||
`export async function main() {
|
||||
return "Test script ${timestamp}";
|
||||
}`);
|
||||
await Deno.writeTextFile(`${tempDir}/f/test/push_script_${timestamp}.script.yaml`,
|
||||
`summary: Test Push Script ${timestamp}
|
||||
description: Script for testing push with override`);
|
||||
|
||||
// Get backend state before push
|
||||
const beforeVariables = await backend.listAllVariables();
|
||||
const beforeScripts = await backend.listAllScripts();
|
||||
|
||||
const variableExistsBefore = beforeVariables.some(v => v.path === `u/admin/test_push_var_${timestamp}`);
|
||||
const scriptExistsBefore = beforeScripts.some(s => s.path === `f/test/push_script_${timestamp}`);
|
||||
|
||||
assertEquals(variableExistsBefore, false, "Variable should not exist before push");
|
||||
assertEquals(scriptExistsBefore, false, "Script should not exist before push");
|
||||
|
||||
// Run sync push (NOT dry-run) to actually push files
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'push',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--yes'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Sync push should succeed: ${result.stderr}`);
|
||||
|
||||
// Verify variable was NOT pushed due to skipVariables: true
|
||||
const afterVariables = await backend.listAllVariables();
|
||||
const variableExistsAfter = afterVariables.some(v => v.path === `u/admin/test_push_var_${timestamp}`);
|
||||
assertEquals(variableExistsAfter, false, "Variable should NOT be pushed due to skipVariables override");
|
||||
|
||||
// Verify script WAS pushed (not affected by skipVariables)
|
||||
const afterScripts = await backend.listAllScripts();
|
||||
const scriptExistsAfter = afterScripts.some(s => s.path === `f/test/push_script_${timestamp}`);
|
||||
assertEquals(scriptExistsAfter, true, "Script should be pushed normally");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Integration: sync pull respects includes override for file filtering", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "includes_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Create wmill.yaml with override that only includes specific path
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
|
||||
overrides:
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
includes:
|
||||
- "u/admin/**" # Only include admin resources, exclude f/** apps/scripts`);
|
||||
|
||||
// Run sync pull to write files
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--yes'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Sync pull should succeed: ${result.stderr}`);
|
||||
|
||||
// Verify admin files were written (since we have includes: ["u/admin/**"])
|
||||
const adminFiles = [];
|
||||
try {
|
||||
for await (const entry of Deno.readDir(`${tempDir}/u/admin`)) {
|
||||
if (entry.isFile) {
|
||||
adminFiles.push(`u/admin/${entry.name}`);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Directory might not exist if no files matched
|
||||
}
|
||||
|
||||
// We expect admin files to be written since backend has u/admin/test_config variable
|
||||
assert(adminFiles.length > 0, `Admin files should be written due to includes override. Expected u/admin files but found: ${adminFiles.join(', ')}`);
|
||||
|
||||
// Verify f/** files were NOT written due to includes override
|
||||
let fDirectoryExists = false;
|
||||
try {
|
||||
await Deno.stat(`${tempDir}/f`);
|
||||
fDirectoryExists = true;
|
||||
} catch {
|
||||
// Directory doesn't exist, which is expected
|
||||
}
|
||||
|
||||
assertEquals(fDirectoryExists, false, "f/ directory should not exist due to includes override excluding f/**");
|
||||
});
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"workspace_id": "starter",
|
||||
"name": "postgres",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"required": [
|
||||
"dbname",
|
||||
"user",
|
||||
"password"
|
||||
],
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string",
|
||||
"description": "The instance host"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"description": "The instance port"
|
||||
},
|
||||
"user": {
|
||||
"type": "string",
|
||||
"description": "The postgres username"
|
||||
},
|
||||
"dbname": {
|
||||
"type": "string",
|
||||
"description": "The database name"
|
||||
},
|
||||
"sslmode": {
|
||||
"enum": [
|
||||
"disable",
|
||||
"allow",
|
||||
"prefer",
|
||||
"require",
|
||||
"verify-ca",
|
||||
"verify-full"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "The sslmode"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"description": "The postgres users password"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "A postgres database connection resource"
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"workspace_id": "starter",
|
||||
"name": "slack",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"required": [],
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string",
|
||||
"description": "The slack token"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "A slack token to interact with a specific workspace. Can be obtained from the OAuth integration in the workspace settings."
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"workspace_id": "starter",
|
||||
"path": "g/all/demodb",
|
||||
"value": {
|
||||
"host": "demodb.service.consul",
|
||||
"port": "6543",
|
||||
"user": "postgres",
|
||||
"dbname": "demodb",
|
||||
"sslmode": "disable",
|
||||
"password": "demodb"
|
||||
},
|
||||
"description": "demodb",
|
||||
"resource_type": "postgres",
|
||||
"extra_perms": {},
|
||||
"is_oauth": false
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { assertEquals, assertStringIncludes, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { readConfigFile, getEffectiveSettings } from "../conf.ts";
|
||||
import { withContainerizedBackend } from "./containerized_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
import { parseJsonFromCLIOutput } from "./test_config_helpers.ts";
|
||||
|
||||
// =============================================================================
|
||||
// SYNC CONFIGURATION RESOLUTION TESTS
|
||||
// Tests for configuration resolution and integration with backend
|
||||
// =============================================================================
|
||||
|
||||
// Helper function to set up workspace profile with localhost_test name
|
||||
async function setupWorkspaceProfile(backend: any): Promise<void> {
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl, // "http://localhost:8001/"
|
||||
workspaceId: backend.workspace, // "test"
|
||||
name: "localhost_test", // This is what the tests expect!
|
||||
token: backend.token
|
||||
};
|
||||
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// INTEGRATION TESTS WITH REAL BACKEND
|
||||
// =============================================================================
|
||||
|
||||
Deno.test("Integration: wmill.yaml configuration produces expected results", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace profile with name "localhost_test"
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Create wmill.yaml with settings
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
- settings.yaml
|
||||
excludes:
|
||||
- "*.test.ts"
|
||||
skipVariables: true
|
||||
skipResources: true
|
||||
includeSettings: true
|
||||
includeSchedules: true
|
||||
includeTriggers: true`);
|
||||
|
||||
// Test pull with wmill.yaml configuration
|
||||
const yamlResult = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir);
|
||||
if (yamlResult.code !== 0) {
|
||||
console.log("YAML command failed!");
|
||||
console.log("Exit code:", yamlResult.code);
|
||||
console.log("Stdout:", yamlResult.stdout);
|
||||
console.log("Stderr:", yamlResult.stderr);
|
||||
}
|
||||
assertEquals(yamlResult.code, 0);
|
||||
|
||||
// Extract JSON from CLI output (skip log messages)
|
||||
const yamlData = parseJsonFromCLIOutput(yamlResult.stdout);
|
||||
|
||||
// Should include settings.yaml due to includeSettings: true
|
||||
const hasSettings = (yamlData.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path === 'settings.yaml'
|
||||
);
|
||||
assertEquals(hasSettings, true);
|
||||
|
||||
// Should NOT include resources or variables (due to skip flags)
|
||||
const hasResources = (yamlData.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path?.includes('.resource.yaml')
|
||||
);
|
||||
const hasVariables = (yamlData.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path?.includes('.variable.yaml')
|
||||
);
|
||||
assertEquals(hasResources, false);
|
||||
assertEquals(hasVariables, false);
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Integration: settings.yaml inclusion respects includeSettings flag", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace profile with name "localhost_test"
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Test 1: includeSettings: true should include settings.yaml
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
includeSettings: true`);
|
||||
|
||||
const includeResult = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir);
|
||||
assertEquals(includeResult.code, 0);
|
||||
|
||||
// Extract JSON from CLI output (skip log messages)
|
||||
const includeData = parseJsonFromCLIOutput(includeResult.stdout);
|
||||
const hasSettingsInclude = (includeData.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path === 'settings.yaml'
|
||||
);
|
||||
assertEquals(hasSettingsInclude, true);
|
||||
|
||||
// Test 2: includeSettings: false should NOT include settings.yaml
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
includeSettings: false`);
|
||||
|
||||
const excludeResult = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir);
|
||||
assertEquals(excludeResult.code, 0);
|
||||
|
||||
// Extract JSON from CLI output (skip log messages)
|
||||
const excludeData = parseJsonFromCLIOutput(excludeResult.stdout);
|
||||
const hasSettingsExclude = (excludeData.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path === 'settings.yaml'
|
||||
);
|
||||
assertEquals(hasSettingsExclude, false);
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Integration: resource/variable filtering respects skip flags", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace profile with name "localhost_test"
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Test skipResources: true
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
skipResources: true
|
||||
skipVariables: false`);
|
||||
|
||||
const result = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir);
|
||||
assertEquals(result.code, 0);
|
||||
|
||||
// Extract JSON from CLI output (skip log messages)
|
||||
const data = parseJsonFromCLIOutput(result.stdout);
|
||||
|
||||
// Should NOT include resources
|
||||
const hasResources = (data.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path?.includes('.resource.yaml')
|
||||
);
|
||||
assertEquals(hasResources, false);
|
||||
|
||||
// Should include variables (not skipped)
|
||||
const hasVariables = (data.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path?.includes('.variable.yaml')
|
||||
);
|
||||
assertEquals(hasVariables, true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { getRootStore } from "../store.ts";
|
||||
|
||||
/**
|
||||
* Create a temporary config directory for testing that doesn't interfere with user's config
|
||||
*/
|
||||
export async function withTestConfig<T>(callback: (testConfigDir: string) => Promise<T>): Promise<T> {
|
||||
// Create a unique temporary directory for this test
|
||||
const testDir = await Deno.makeTempDir({ prefix: "wmill_test_config_" });
|
||||
|
||||
try {
|
||||
return await callback(testDir);
|
||||
} finally {
|
||||
// Clean up the temporary directory
|
||||
try {
|
||||
await Deno.remove(testDir, { recursive: true });
|
||||
} catch (error) {
|
||||
console.warn(`Failed to clean up test config directory ${testDir}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the remotes file in test config directory
|
||||
*/
|
||||
export async function clearTestRemotes(testConfigDir: string): Promise<void> {
|
||||
const remoteFile = (await getRootStore(testConfigDir)) + "remotes.ndjson";
|
||||
await Deno.writeTextFile(remoteFile, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JSON output from CLI command, handling log messages that appear before JSON
|
||||
*/
|
||||
export function parseJsonFromCLIOutput(stdout: string): any {
|
||||
const jsonMatch = stdout.match(/\{[\s\S]*\}/);
|
||||
if (!jsonMatch) {
|
||||
throw new Error(`No JSON found in CLI output: ${stdout}`);
|
||||
}
|
||||
return JSON.parse(jsonMatch[0]);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { assertEquals, assertRejects } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { addWorkspace, allWorkspaces } from "../workspace.ts";
|
||||
import { withTestConfig, clearTestRemotes } from "./test_config_helpers.ts";
|
||||
|
||||
// Test workspace conflict detection
|
||||
Deno.test("addWorkspace: prevents duplicate workspace names", async () => {
|
||||
await withTestConfig(async (testConfigDir) => {
|
||||
await clearTestRemotes(testConfigDir);
|
||||
|
||||
// Add first workspace
|
||||
const workspace1 = {
|
||||
name: "test_workspace",
|
||||
remote: "http://localhost:8001/",
|
||||
workspaceId: "workspace1",
|
||||
token: "token1"
|
||||
};
|
||||
|
||||
await addWorkspace(workspace1, { force: true, configDir: testConfigDir });
|
||||
|
||||
// Try to add workspace with same name but different details
|
||||
const workspace2 = {
|
||||
name: "test_workspace", // Same name
|
||||
remote: "http://localhost:8002/", // Different remote
|
||||
workspaceId: "workspace2", // Different ID
|
||||
token: "token2"
|
||||
};
|
||||
|
||||
// Should throw error in non-interactive mode without force
|
||||
await assertRejects(
|
||||
() => addWorkspace(workspace2, { configDir: testConfigDir }),
|
||||
Error,
|
||||
"Workspace name conflict. Use --force to overwrite or choose a different name."
|
||||
);
|
||||
|
||||
// Should succeed with force flag
|
||||
await addWorkspace(workspace2, { force: true, configDir: testConfigDir });
|
||||
|
||||
// Verify the workspace was overwritten
|
||||
const workspaces = await allWorkspaces(testConfigDir);
|
||||
assertEquals(workspaces.length, 1);
|
||||
assertEquals(workspaces[0].name, "test_workspace");
|
||||
assertEquals(workspaces[0].remote, "http://localhost:8002/");
|
||||
assertEquals(workspaces[0].workspaceId, "workspace2");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("addWorkspace: prevents duplicate (remote, workspaceId) tuples", async () => {
|
||||
await withTestConfig(async (testConfigDir) => {
|
||||
await clearTestRemotes(testConfigDir);
|
||||
|
||||
// Add first workspace
|
||||
const workspace1 = {
|
||||
name: "first_workspace",
|
||||
remote: "http://localhost:8001/",
|
||||
workspaceId: "test",
|
||||
token: "token1"
|
||||
};
|
||||
|
||||
await addWorkspace(workspace1, { force: true, configDir: testConfigDir });
|
||||
|
||||
// Try to add workspace with same (remote, workspaceId) but different name
|
||||
const workspace2 = {
|
||||
name: "second_workspace", // Different name
|
||||
remote: "http://localhost:8001/", // Same remote
|
||||
workspaceId: "test", // Same workspaceId
|
||||
token: "token2"
|
||||
};
|
||||
|
||||
// Should throw error in non-interactive mode without force
|
||||
await assertRejects(
|
||||
() => addWorkspace(workspace2, { configDir: testConfigDir }),
|
||||
Error,
|
||||
'Backend constraint violation: (http://localhost:8001/, test) already exists as "first_workspace". Use --force to overwrite.'
|
||||
);
|
||||
|
||||
// Should succeed with force flag (overwrites first workspace)
|
||||
await addWorkspace(workspace2, { force: true, configDir: testConfigDir });
|
||||
|
||||
// Verify the first workspace was removed and second was added
|
||||
const workspaces = await allWorkspaces(testConfigDir);
|
||||
assertEquals(workspaces.length, 1);
|
||||
assertEquals(workspaces[0].name, "second_workspace");
|
||||
assertEquals(workspaces[0].remote, "http://localhost:8001/");
|
||||
assertEquals(workspaces[0].workspaceId, "test");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("addWorkspace: allows same workspace (name, remote, workspaceId) with token update", async () => {
|
||||
await withTestConfig(async (testConfigDir) => {
|
||||
await clearTestRemotes(testConfigDir);
|
||||
|
||||
// Add first workspace
|
||||
const workspace1 = {
|
||||
name: "same_workspace",
|
||||
remote: "http://localhost:8001/",
|
||||
workspaceId: "test",
|
||||
token: "old_token"
|
||||
};
|
||||
|
||||
await addWorkspace(workspace1, { force: true, configDir: testConfigDir });
|
||||
|
||||
// Add same workspace with updated token
|
||||
const workspace2 = {
|
||||
name: "same_workspace", // Same name
|
||||
remote: "http://localhost:8001/", // Same remote
|
||||
workspaceId: "test", // Same workspaceId
|
||||
token: "new_token" // Different token
|
||||
};
|
||||
|
||||
// Should succeed without force (just token update)
|
||||
await addWorkspace(workspace2, { configDir: testConfigDir });
|
||||
|
||||
// Verify token was updated
|
||||
const workspaces = await allWorkspaces(testConfigDir);
|
||||
assertEquals(workspaces.length, 1);
|
||||
assertEquals(workspaces[0].name, "same_workspace");
|
||||
assertEquals(workspaces[0].token, "new_token");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("addWorkspace: allows different workspaces on different remotes", async () => {
|
||||
await withTestConfig(async (testConfigDir) => {
|
||||
await clearTestRemotes(testConfigDir);
|
||||
|
||||
// Add workspace on first remote
|
||||
const workspace1 = {
|
||||
name: "workspace_remote1",
|
||||
remote: "http://localhost:8001/",
|
||||
workspaceId: "test",
|
||||
token: "token1"
|
||||
};
|
||||
|
||||
await addWorkspace(workspace1, { force: true, configDir: testConfigDir });
|
||||
|
||||
// Add workspace with same workspaceId on different remote (should be allowed)
|
||||
const workspace2 = {
|
||||
name: "workspace_remote2",
|
||||
remote: "http://localhost:8002/", // Different remote
|
||||
workspaceId: "test", // Same workspaceId (OK on different remote)
|
||||
token: "token2"
|
||||
};
|
||||
|
||||
// Should succeed (different remotes)
|
||||
await addWorkspace(workspace2, { configDir: testConfigDir });
|
||||
|
||||
// Verify both workspaces exist
|
||||
const workspaces = await allWorkspaces(testConfigDir);
|
||||
assertEquals(workspaces.length, 2);
|
||||
|
||||
const names = workspaces.map(w => w.name).sort();
|
||||
assertEquals(names, ["workspace_remote1", "workspace_remote2"]);
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -49,6 +49,7 @@ export type GlobalOptions = {
|
||||
baseUrl: string | undefined;
|
||||
workspace: string | undefined;
|
||||
token: string | undefined;
|
||||
configDir: string | undefined;
|
||||
};
|
||||
|
||||
export function isSuperset(
|
||||
@@ -231,12 +232,11 @@ export function getTypeStrFromPath(
|
||||
parsed.ext == ".cs" ||
|
||||
parsed.ext == ".nu" ||
|
||||
parsed.ext == ".java" ||
|
||||
// for related places search: ADD_NEW_LANG
|
||||
// for related places search: ADD_NEW_LANG
|
||||
(parsed.ext == ".yml" && parsed.name.split(".").pop() == "playbook")
|
||||
) {
|
||||
return "script";
|
||||
}
|
||||
|
||||
if (parsed.name === "folder.meta") {
|
||||
return "folder";
|
||||
}
|
||||
|
||||
@@ -149,4 +149,51 @@ export function printSync(input: string | Uint8Array, to = Deno.stdout) {
|
||||
while (bytesWritten < bytes.length) {
|
||||
bytesWritten += to.writeSync(bytes.subarray(bytesWritten))
|
||||
}
|
||||
}
|
||||
|
||||
// Repository interface for shared selection logic
|
||||
export interface Repository {
|
||||
git_repo_resource_path: string;
|
||||
}
|
||||
|
||||
// Shared repository selection logic
|
||||
export async function selectRepository<T extends Repository>(
|
||||
repositories: T[],
|
||||
operation?: string
|
||||
): Promise<T> {
|
||||
if (repositories.length === 0) {
|
||||
throw new Error("No git-sync repositories configured in workspace");
|
||||
}
|
||||
|
||||
if (repositories.length === 1) {
|
||||
const repoPath = repositories[0].git_repo_resource_path.replace(/^\$res:/, "");
|
||||
log.info(`Using repository: ${repoPath}`);
|
||||
return repositories[0];
|
||||
}
|
||||
|
||||
// Check if we're in a non-interactive environment
|
||||
const isInteractive = Deno.stdin.isTerminal() && Deno.stdout.isTerminal();
|
||||
|
||||
if (!isInteractive) {
|
||||
const repoPaths = repositories.map(r => r.git_repo_resource_path.replace(/^\$res:/, ""));
|
||||
throw new Error(`Multiple repositories found: ${repoPaths.join(', ')}. Use --repository to specify which one to ${operation || 'use'}.`);
|
||||
}
|
||||
|
||||
// Import Select dynamically to avoid dependency issues
|
||||
const { Select } = await import("./deps.ts");
|
||||
|
||||
console.log(`\nMultiple repositories found. Please select which repository to ${operation || 'use'}:\n`);
|
||||
|
||||
const selectedRepo = await Select.prompt({
|
||||
message: `Select repository for ${operation || 'operation'}:`,
|
||||
options: repositories.map((repo, index) => {
|
||||
const displayPath = repo.git_repo_resource_path.replace(/^\$res:/, "");
|
||||
return {
|
||||
name: `${index + 1}. ${displayPath}`,
|
||||
value: repo.git_repo_resource_path
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
return repositories.find((r) => r.git_repo_resource_path === selectedRepo)!;
|
||||
}
|
||||
+94
-16
@@ -2,7 +2,7 @@
|
||||
import { GlobalOptions } from "./types.ts";
|
||||
import { getRootStore } from "./store.ts";
|
||||
import { loginInteractive, tryGetLoginInfo } from "./login.ts";
|
||||
import { colors, Command, Input, log, setClient, Table } from "./deps.ts";
|
||||
import { colors, Command, Confirm, Input, log, setClient, Table } from "./deps.ts";
|
||||
|
||||
import * as wmill from "./gen/services.gen.ts";
|
||||
import { requireLogin } from "./context.ts";
|
||||
@@ -14,9 +14,9 @@ export interface Workspace {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export async function allWorkspaces(): Promise<Workspace[]> {
|
||||
export async function allWorkspaces(configDirOverride?: string): Promise<Workspace[]> {
|
||||
try {
|
||||
const file = (await getRootStore()) + "remotes.ndjson";
|
||||
const file = (await getRootStore(configDirOverride)) + "remotes.ndjson";
|
||||
const txt = await Deno.readTextFile(file);
|
||||
return txt
|
||||
.split("\n")
|
||||
@@ -40,7 +40,7 @@ async function getActiveWorkspaceName(
|
||||
return opts?.workspace;
|
||||
}
|
||||
try {
|
||||
return await Deno.readTextFile((await getRootStore()) + "/activeWorkspace");
|
||||
return await Deno.readTextFile((await getRootStore(opts?.configDir)) + "/activeWorkspace");
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
@@ -53,13 +53,14 @@ export async function getActiveWorkspace(
|
||||
if (!name) {
|
||||
return undefined;
|
||||
}
|
||||
return await getWorkspaceByName(name);
|
||||
return await getWorkspaceByName(name, opts?.configDir);
|
||||
}
|
||||
|
||||
export async function getWorkspaceByName(
|
||||
workspaceName: string
|
||||
workspaceName: string,
|
||||
configDirOverride?: string
|
||||
): Promise<Workspace | undefined> {
|
||||
const workspaceStream = await allWorkspaces();
|
||||
const workspaceStream = await allWorkspaces(configDirOverride);
|
||||
for await (const workspace of workspaceStream) {
|
||||
if (workspace.name === workspaceName) {
|
||||
return workspace;
|
||||
@@ -69,7 +70,7 @@ export async function getWorkspaceByName(
|
||||
}
|
||||
|
||||
async function list(opts: GlobalOptions) {
|
||||
const workspaces = await allWorkspaces();
|
||||
const workspaces = await allWorkspaces(opts.configDir);
|
||||
const activeName = await getActiveWorkspaceName(opts);
|
||||
|
||||
new Table()
|
||||
@@ -101,7 +102,7 @@ async function switchC(opts: GlobalOptions, workspaceName: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
const all = await allWorkspaces();
|
||||
const all = await allWorkspaces(opts.configDir);
|
||||
if (all.findIndex((x) => x.name === workspaceName) === -1) {
|
||||
log.info(
|
||||
colors.red.bold(
|
||||
@@ -115,13 +116,13 @@ async function switchC(opts: GlobalOptions, workspaceName: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
await setActiveWorkspace(workspaceName);
|
||||
await setActiveWorkspace(workspaceName, opts.configDir);
|
||||
return;
|
||||
}
|
||||
|
||||
export async function setActiveWorkspace(workspaceName: string) {
|
||||
export async function setActiveWorkspace(workspaceName: string, configDirOverride?: string) {
|
||||
await Deno.writeTextFile(
|
||||
(await getRootStore()) + "/activeWorkspace",
|
||||
(await getRootStore(configDirOverride)) + "/activeWorkspace",
|
||||
workspaceName
|
||||
);
|
||||
}
|
||||
@@ -246,7 +247,7 @@ export async function add(
|
||||
},
|
||||
opts
|
||||
);
|
||||
await setActiveWorkspace(workspaceName);
|
||||
await setActiveWorkspace(workspaceName, opts.configDir);
|
||||
|
||||
log.info(
|
||||
colors.green.underline(
|
||||
@@ -257,8 +258,85 @@ export async function add(
|
||||
|
||||
export async function addWorkspace(workspace: Workspace, opts: any) {
|
||||
workspace.remote = new URL(workspace.remote).toString(); // add trailing slash in all cases!
|
||||
|
||||
// Check for conflicts before adding
|
||||
const existingWorkspaces = await allWorkspaces(opts.configDir);
|
||||
const isInteractive = Deno.stdin.isTerminal() && Deno.stdout.isTerminal() && !opts.force;
|
||||
|
||||
// Check 1: Workspace name already exists
|
||||
const nameConflict = existingWorkspaces.find(w => w.name === workspace.name);
|
||||
if (nameConflict) {
|
||||
// If it's the exact same workspace (same remote + workspaceId), just update the token
|
||||
if (nameConflict.remote === workspace.remote && nameConflict.workspaceId === workspace.workspaceId) {
|
||||
log.info(colors.yellow(`Updating token for existing workspace "${workspace.name}"`));
|
||||
} else {
|
||||
// Different remote or workspaceId - this is a conflict
|
||||
log.info(colors.red.bold(`❌ Workspace name "${workspace.name}" already exists!`));
|
||||
log.info(` Existing: ${nameConflict.workspaceId} on ${nameConflict.remote}`);
|
||||
log.info(` New: ${workspace.workspaceId} on ${workspace.remote}`);
|
||||
|
||||
if (!isInteractive) {
|
||||
// In non-interactive mode (tests, scripts), auto-overwrite with force flag
|
||||
if (opts.force) {
|
||||
log.info(colors.yellow("Force flag enabled, overwriting existing workspace."));
|
||||
} else {
|
||||
throw new Error("Workspace name conflict. Use --force to overwrite or choose a different name.");
|
||||
}
|
||||
} else {
|
||||
const overwrite = await Confirm.prompt({
|
||||
message: "Do you want to overwrite the existing workspace?",
|
||||
default: false,
|
||||
});
|
||||
|
||||
if (!overwrite) {
|
||||
log.info(colors.yellow("Operation cancelled."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check 2: Same (remote, workspaceId) tuple already exists under different name
|
||||
const tupleConflict = existingWorkspaces.find(w =>
|
||||
w.remote === workspace.remote &&
|
||||
w.workspaceId === workspace.workspaceId &&
|
||||
w.name !== workspace.name
|
||||
);
|
||||
|
||||
if (tupleConflict) {
|
||||
log.info(colors.red.bold(`❌ Workspace ${workspace.workspaceId} on ${workspace.remote} already exists!`));
|
||||
log.info(` Existing name: "${tupleConflict.name}"`);
|
||||
log.info(` New name: "${workspace.name}"`);
|
||||
log.info(colors.yellow(`\nNote: Backend constraint prevents duplicate (remote, workspaceId) combinations.`));
|
||||
|
||||
if (!isInteractive) {
|
||||
// In non-interactive mode (tests, scripts), auto-overwrite with force flag
|
||||
if (opts.force) {
|
||||
log.info(colors.yellow(`Force flag enabled, overwriting existing workspace "${tupleConflict.name}".`));
|
||||
} else {
|
||||
throw new Error(`Backend constraint violation: (${workspace.remote}, ${workspace.workspaceId}) already exists as "${tupleConflict.name}". Use --force to overwrite.`);
|
||||
}
|
||||
} else {
|
||||
const overwrite = await Confirm.prompt({
|
||||
message: `Do you want to overwrite the existing workspace "${tupleConflict.name}"?`,
|
||||
default: false,
|
||||
});
|
||||
|
||||
if (!overwrite) {
|
||||
log.info(colors.yellow("Operation cancelled."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the conflicting workspace
|
||||
await removeWorkspace(tupleConflict.name, true, opts);
|
||||
}
|
||||
|
||||
// Remove existing workspace with same name (if updating)
|
||||
await removeWorkspace(workspace.name, true, opts);
|
||||
const file = await Deno.open((await getRootStore()) + "remotes.ndjson", {
|
||||
|
||||
// Add the new workspace
|
||||
const file = await Deno.open((await getRootStore(opts.configDir)) + "remotes.ndjson", {
|
||||
append: true,
|
||||
write: true,
|
||||
read: true,
|
||||
@@ -274,7 +352,7 @@ export async function removeWorkspace(
|
||||
silent: boolean,
|
||||
opts: any
|
||||
) {
|
||||
const orgWorkspaces = await allWorkspaces();
|
||||
const orgWorkspaces = await allWorkspaces(opts.configDir);
|
||||
if (orgWorkspaces.findIndex((x) => x.name === name) === -1) {
|
||||
if (!silent) {
|
||||
log.info(
|
||||
@@ -290,7 +368,7 @@ export async function removeWorkspace(
|
||||
}
|
||||
|
||||
await Deno.writeTextFile(
|
||||
(await getRootStore()) + "remotes.ndjson",
|
||||
(await getRootStore(opts.configDir)) + "remotes.ndjson",
|
||||
orgWorkspaces
|
||||
.filter((x) => x.name !== name)
|
||||
.map((x) => JSON.stringify(x))
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.505.3",
|
||||
"version": "1.507.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.505.3",
|
||||
"version": "1.507.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.505.3",
|
||||
"version": "1.507.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
import GfmMarkdown from './GfmMarkdown.svelte'
|
||||
import { apiTokenApps, forceSecretValue, linkedSecretValue } from './app_connect'
|
||||
import type { SchemaProperty } from '$lib/common'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
interface Props {
|
||||
step?: number
|
||||
@@ -94,6 +95,27 @@
|
||||
let path: string = $state('')
|
||||
let description = $state('')
|
||||
|
||||
/**
|
||||
* Client credentials OAuth flow support
|
||||
* @description Determines if the selected OAuth provider supports client_credentials grant type
|
||||
* alongside the traditional authorization_code flow
|
||||
*/
|
||||
let supportsClientCredentials = $state(false)
|
||||
|
||||
/**
|
||||
* OAuth flow selection
|
||||
* @description Controls which OAuth flow to use:
|
||||
* - false: authorization_code flow (interactive, requires user consent)
|
||||
* - true: client_credentials flow (server-to-server, no user interaction)
|
||||
*/
|
||||
let useClientCredentials = $state(false)
|
||||
|
||||
/**
|
||||
* Client credentials for resource-level OAuth
|
||||
*/
|
||||
let clientId = $state('')
|
||||
let clientSecret = $state('')
|
||||
|
||||
let resourceTypeInfo: ResourceType | undefined = $state(undefined)
|
||||
|
||||
let pathError = $state('')
|
||||
@@ -107,6 +129,13 @@
|
||||
description = ''
|
||||
resourceType = rt ?? ''
|
||||
valueToken = undefined
|
||||
|
||||
// Reset client credentials state
|
||||
supportsClientCredentials = false
|
||||
useClientCredentials = false
|
||||
clientId = ''
|
||||
clientSecret = ''
|
||||
|
||||
await loadConnects()
|
||||
manual = !connects?.includes(resourceType)
|
||||
if (manual && express) {
|
||||
@@ -151,13 +180,15 @@
|
||||
disabled =
|
||||
(step == 1 && resourceType == '') ||
|
||||
(step == 2 &&
|
||||
value == '' &&
|
||||
args &&
|
||||
args['token'] == '' &&
|
||||
args['password'] == '' &&
|
||||
args['api_key'] == '' &&
|
||||
args['key'] == '' &&
|
||||
linkedSecret != undefined) ||
|
||||
(manual
|
||||
? value == '' &&
|
||||
args &&
|
||||
args['token'] == '' &&
|
||||
args['password'] == '' &&
|
||||
args['api_key'] == '' &&
|
||||
args['key'] == '' &&
|
||||
linkedSecret != undefined
|
||||
: false)) ||
|
||||
step == 3 ||
|
||||
(step == 4 && pathError != '') ||
|
||||
!isValid
|
||||
@@ -259,6 +290,12 @@
|
||||
const connect = await OauthService.getOauthConnect({ client: resourceType })
|
||||
scopes = connect.scopes ?? []
|
||||
extra_params = Object.entries(connect.extra_params ?? {}) as [string, string][]
|
||||
|
||||
/**
|
||||
* Check if the OAuth provider supports client_credentials grant type
|
||||
* This determines whether to show the OAuth flow selection UI
|
||||
*/
|
||||
supportsClientCredentials = connect.grant_types?.includes('client_credentials') ?? false
|
||||
}
|
||||
|
||||
async function getResourceTypeInfo() {
|
||||
@@ -287,21 +324,68 @@
|
||||
}
|
||||
step += 1
|
||||
} else if (step == 2 && !manual) {
|
||||
const url = new URL(`/api/oauth/connect/${resourceType}`, window.location.origin)
|
||||
url.searchParams.append('scopes', scopes.join('+'))
|
||||
if (extra_params.length > 0) {
|
||||
extra_params.forEach(([key, value]) => url.searchParams.append(key, value))
|
||||
}
|
||||
// if (!newPageOAuth) {
|
||||
// window.location.href = url.toString()
|
||||
// } else {
|
||||
window.addEventListener('message', popupListener)
|
||||
window.addEventListener('storage', handleStorageEvent)
|
||||
window.open(url.toString(), '_blank', 'popup=true')
|
||||
step += 1
|
||||
if (useClientCredentials) {
|
||||
/**
|
||||
* Client credentials flow: Direct API call to backend
|
||||
* No popup window or user interaction required
|
||||
* Uses instance-level OAuth credentials for server-to-server auth
|
||||
*/
|
||||
try {
|
||||
// Trim whitespace from credentials to avoid false negatives
|
||||
const trimmedClientId = clientId.trim()
|
||||
const trimmedClientSecret = clientSecret.trim()
|
||||
|
||||
// dispatch('close')
|
||||
// }
|
||||
// Validate required fields
|
||||
if (!trimmedClientId || !trimmedClientSecret) {
|
||||
sendUserToast(
|
||||
'Client ID and Client Secret are required for client credentials flow',
|
||||
true
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const tokenResponse = await OauthService.connectClientCredentials({
|
||||
client: resourceType,
|
||||
requestBody: {
|
||||
scopes: scopes,
|
||||
cc_client_id: trimmedClientId,
|
||||
cc_client_secret: trimmedClientSecret
|
||||
}
|
||||
})
|
||||
|
||||
// Process the token response like in popup flow
|
||||
value = tokenResponse.access_token!
|
||||
valueToken = {
|
||||
...tokenResponse,
|
||||
grant_type: 'client_credentials' // Mark this token as client_credentials
|
||||
}
|
||||
step = 4
|
||||
if (express) {
|
||||
path = `u/${$userStore?.username}/${resourceType}_${new Date().getTime()}`
|
||||
next()
|
||||
}
|
||||
} catch (error) {
|
||||
sendUserToast(
|
||||
`Failed to connect with client credentials: ${error.body || error.message}`,
|
||||
true
|
||||
)
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* Authorization code flow: Traditional OAuth popup window
|
||||
* Requires user interaction and consent
|
||||
* Opens popup for user to authenticate with OAuth provider
|
||||
*/
|
||||
const url = new URL(`/api/oauth/connect/${resourceType}`, window.location.origin)
|
||||
url.searchParams.append('scopes', scopes.join('+'))
|
||||
if (extra_params.length > 0) {
|
||||
extra_params.forEach(([key, value]) => url.searchParams.append(key, value))
|
||||
}
|
||||
window.addEventListener('message', popupListener)
|
||||
window.addEventListener('storage', handleStorageEvent)
|
||||
window.open(url.toString(), '_blank', 'popup=true')
|
||||
step += 1
|
||||
}
|
||||
} else {
|
||||
if (!path) {
|
||||
throw Error('Path is not set')
|
||||
@@ -331,14 +415,23 @@
|
||||
|
||||
let account: number | undefined = undefined
|
||||
if (valueToken?.expires_in != undefined) {
|
||||
const accountData: any = {
|
||||
refresh_token: valueToken.refresh_token ?? '',
|
||||
expires_in: valueToken.expires_in,
|
||||
client: resourceType,
|
||||
grant_type: valueToken.grant_type || 'authorization_code'
|
||||
}
|
||||
|
||||
// Add client credentials if using client_credentials flow
|
||||
if (useClientCredentials) {
|
||||
accountData.cc_client_id = clientId.trim()
|
||||
accountData.cc_client_secret = clientSecret.trim()
|
||||
}
|
||||
|
||||
account = Number(
|
||||
await OauthService.createAccount({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
refresh_token: valueToken.refresh_token ?? '',
|
||||
expires_in: valueToken.expires_in,
|
||||
client: resourceType
|
||||
}
|
||||
requestBody: accountData
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -604,14 +697,58 @@
|
||||
{#if manual == false && resourceType != ''}
|
||||
<h1 class="mb-4">{resourceType}</h1>
|
||||
<div class="my-4 text-secondary"
|
||||
>Click connect to create a resource backed by an oauth connection, whose token is fetched
|
||||
from the external services and refreshed automatically if needed before expiration (using
|
||||
its refresh token)</div
|
||||
>Create a resource backed by an OAuth connection, whose token is fetched from the external
|
||||
services and refreshed automatically if needed before expiration.</div
|
||||
>
|
||||
<h4 class="mb-2">Description</h4>
|
||||
<div class="text-sm mb-8">
|
||||
<Markdown md={urlize(resourceTypeInfo?.description ?? '', 'md')} />
|
||||
</div>
|
||||
|
||||
{#if supportsClientCredentials}
|
||||
<div style="margin-bottom: 16px;">
|
||||
<h3 class="mb-4">Authentication Method</h3>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<input
|
||||
type="checkbox"
|
||||
style="width: 16px; height: 16px; margin: 0;"
|
||||
bind:checked={useClientCredentials}
|
||||
/>
|
||||
<span style="font-size: 14px; font-weight: 600;">Use Client Credentials Flow</span>
|
||||
<Tooltip>
|
||||
Server-to-server authentication without user interaction.
|
||||
<br /><br />
|
||||
Provide your own OAuth client credentials for this resource.
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{#if useClientCredentials}
|
||||
<div style="margin-top: 16px;">
|
||||
<label style="display: block; margin-bottom: 8px;">
|
||||
<span style="font-weight: 600;">Client ID</span>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={clientId}
|
||||
placeholder="Enter OAuth client ID"
|
||||
class="w-full p-2 border border-gray-300 rounded mt-1"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label style="display: block;">
|
||||
<span style="font-weight: 600;">Client Secret</span>
|
||||
<input
|
||||
type="password"
|
||||
bind:value={clientSecret}
|
||||
placeholder="Enter OAuth client secret"
|
||||
class="w-full p-2 border border-gray-300 rounded mt-1"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<h3 class="mb-4 flex gap-4"
|
||||
>Scopes <button
|
||||
onclick={() => {
|
||||
@@ -631,7 +768,11 @@
|
||||
{/if}
|
||||
{/if}
|
||||
{:else if step == 3 && !manual && !express}
|
||||
Finish connection in popup window
|
||||
{#if useClientCredentials}
|
||||
Connecting with client credentials...
|
||||
{:else}
|
||||
Finish connection in popup window
|
||||
{/if}
|
||||
{:else}
|
||||
<Path
|
||||
initialPath=""
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
emptyString,
|
||||
getSchemaFromProperties
|
||||
} from '$lib/utils'
|
||||
import { DollarSign, Pipette, Plus, X, Check, Loader2 } from 'lucide-svelte'
|
||||
import { DollarSign, Plus, X, Check, Loader2 } from 'lucide-svelte'
|
||||
import { createEventDispatcher, onDestroy, onMount, tick, untrack } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { Button, SecondsInput } from './common'
|
||||
@@ -25,7 +25,6 @@
|
||||
import ArgEnum from './ArgEnum.svelte'
|
||||
import DateTimeInput from './DateTimeInput.svelte'
|
||||
import DateInput from './DateInput.svelte'
|
||||
import S3FilePicker from './S3FilePicker.svelte'
|
||||
import CurrencyInput from './apps/components/inputs/currency/CurrencyInput.svelte'
|
||||
import FileUpload from './common/fileUpload/FileUpload.svelte'
|
||||
import autosize from '$lib/autosize'
|
||||
@@ -42,6 +41,7 @@
|
||||
import type { ComponentCustomCSS } from './apps/types'
|
||||
import MultiSelect from './select/MultiSelect.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
import S3ArgInput from './common/fileUpload/S3ArgInput.svelte'
|
||||
|
||||
interface Props {
|
||||
label?: string
|
||||
@@ -227,8 +227,6 @@
|
||||
|
||||
let ignoreValueUndefined = $state(false)
|
||||
let error: string = $state('')
|
||||
let s3FilePicker: S3FilePicker | undefined = $state()
|
||||
let s3FileUploadRawMode: boolean = $state(false)
|
||||
let isListJson = $state(false)
|
||||
let hasIsListJsonChanged = $state(false)
|
||||
|
||||
@@ -499,16 +497,6 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<S3FilePicker
|
||||
bind:this={s3FilePicker}
|
||||
bind:selectedFileKey={value}
|
||||
on:close={() => {
|
||||
rawValue = JSON.stringify(value, null, 2)
|
||||
editor?.setCode(rawValue)
|
||||
}}
|
||||
readOnlyMode={false}
|
||||
/>
|
||||
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<div
|
||||
class={twMerge(
|
||||
@@ -631,6 +619,22 @@
|
||||
<span> Not set</span>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if (inputCat == 'resource-object' && format && format.split('-').length > 1 && format
|
||||
.replace('resource-', '')
|
||||
.replace('_', '')
|
||||
.toLowerCase() == 's3object') || (inputCat == 'list' && itemsType?.resourceType === 's3_object')}
|
||||
<S3ArgInput
|
||||
multiple={inputCat == 'list'}
|
||||
bind:value
|
||||
{defaultValue}
|
||||
{setNewValueFromCode}
|
||||
{workspace}
|
||||
onFocus={() => dispatch('focus')}
|
||||
onBlur={() => dispatch('blur')}
|
||||
bind:editor
|
||||
{appPath}
|
||||
{computeS3ForceViewerPolicies}
|
||||
/>
|
||||
{:else if inputCat == 'list' && !isListJson}
|
||||
<div class="w-full flex gap-4">
|
||||
<div class="w-full">
|
||||
@@ -851,70 +855,6 @@
|
||||
}}
|
||||
{showSchemaExplorer}
|
||||
/>
|
||||
{:else if inputCat == 'resource-object' && format && format.split('-').length > 1 && format
|
||||
.replace('resource-', '')
|
||||
.replace('_', '')
|
||||
.toLowerCase() == 's3object'}
|
||||
<div class="flex flex-col w-full gap-1">
|
||||
<Toggle
|
||||
class="flex justify-end"
|
||||
bind:checked={s3FileUploadRawMode}
|
||||
size="xs"
|
||||
options={{ left: 'Raw S3 object input' }}
|
||||
/>
|
||||
{#if s3FileUploadRawMode}
|
||||
{#await import('$lib/components/JsonEditor.svelte')}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then Module}
|
||||
<Module.default
|
||||
bind:editor
|
||||
on:focus={(e) => {
|
||||
dispatch('focus')
|
||||
}}
|
||||
on:blur={(e) => {
|
||||
dispatch('blur')
|
||||
}}
|
||||
code={JSON.stringify(value ?? defaultValue ?? { s3: '' }, null, 2)}
|
||||
on:changeValue={(e) => {
|
||||
setNewValueFromCode(e.detail)
|
||||
}}
|
||||
/>
|
||||
{/await}
|
||||
{:else}
|
||||
<FileUpload
|
||||
{appPath}
|
||||
computeForceViewerPolicies={computeS3ForceViewerPolicies}
|
||||
{workspace}
|
||||
allowMultiple={false}
|
||||
randomFileKey={true}
|
||||
on:addition={(evt) => {
|
||||
value = {
|
||||
s3: evt.detail?.path ?? '',
|
||||
filename: evt.detail?.filename ?? ''
|
||||
}
|
||||
}}
|
||||
on:deletion={(evt) => {
|
||||
value = {
|
||||
s3: ''
|
||||
}
|
||||
}}
|
||||
defaultValue={defaultValue?.s3}
|
||||
initialValue={value}
|
||||
/>
|
||||
{/if}
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
btnClasses="mt-1"
|
||||
on:click={() => {
|
||||
s3FilePicker?.open?.(value)
|
||||
}}
|
||||
startIcon={{ icon: Pipette }}
|
||||
>
|
||||
Choose an object from the catalog
|
||||
</Button>
|
||||
</div>
|
||||
{:else if inputCat == 'object' || inputCat == 'resource-object' || isListJson}
|
||||
{#if oneOf && oneOf.length >= 2}
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { ExternalLink, Plus } from 'lucide-svelte'
|
||||
import AzureOauthSettings from './AzureOauthSettings.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
interface Props {
|
||||
snowflakeAccountIdentifier?: string
|
||||
@@ -233,6 +234,49 @@
|
||||
<span class="text-primary font-semibold text-sm">Client Secret</span>
|
||||
<input type="text" placeholder="Client Secret" bind:value={oauths[k]['secret']} />
|
||||
</label>
|
||||
{#if k === 'visma' || !windmillBuiltins.includes(k)}
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<input
|
||||
type="checkbox"
|
||||
style="width: 16px; height: 16px; margin: 0;"
|
||||
checked={oauths?.[k]?.['grant_types']?.includes('client_credentials') ??
|
||||
false}
|
||||
onchange={(e) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
if (oauths && oauths[k]) {
|
||||
if (!oauths[k]['grant_types']) {
|
||||
oauths[k]['grant_types'] = ['authorization_code']
|
||||
}
|
||||
if (target.checked) {
|
||||
if (!oauths[k]['grant_types'].includes('client_credentials')) {
|
||||
oauths[k]['grant_types'] = [
|
||||
...oauths[k]['grant_types'],
|
||||
'client_credentials'
|
||||
]
|
||||
}
|
||||
} else {
|
||||
oauths[k]['grant_types'] = oauths[k]['grant_types'].filter(
|
||||
(gt) => gt !== 'client_credentials'
|
||||
)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span style="font-size: 14px; font-weight: 600;"
|
||||
>Support Client Credentials Flow</span
|
||||
>
|
||||
<Tooltip>
|
||||
Enables server-to-server authentication without user interaction. Use for
|
||||
automated scripts and background jobs.
|
||||
<br /><br />
|
||||
When enabled, users can provide their own client credentials at the resource
|
||||
level. The Client ID and Secret configured above are only used for the traditional
|
||||
OAuth flow (popup window).
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if k === 'azure_oauth'}
|
||||
<AzureOauthSettings bind:connect_config={oauths[k]['connect_config']} />
|
||||
{:else if !windmillBuiltins.includes(k) && k != 'slack'}
|
||||
@@ -285,7 +329,7 @@
|
||||
on:click={() => {
|
||||
if (oauths) {
|
||||
let name = oauth_name == 'custom' ? resourceName : oauth_name
|
||||
oauths[name ?? ''] = { id: '', secret: '' }
|
||||
oauths[name ?? ''] = { id: '', secret: '', grant_types: ['authorization_code'] }
|
||||
}
|
||||
resourceName = ''
|
||||
}}
|
||||
|
||||
@@ -113,7 +113,8 @@
|
||||
|
||||
// Flow preview
|
||||
let flowPreviewButtons: FlowPreviewButtons | undefined = $state()
|
||||
const job: Job | undefined = $derived(flowPreviewButtons?.getJob())
|
||||
const flowPreviewContent = $derived(flowPreviewButtons?.getFlowPreviewContent())
|
||||
const job: Job | undefined = $derived(flowPreviewContent?.getJob())
|
||||
let showJobStatus = $state(false)
|
||||
let testModuleId: string | undefined = $state(undefined)
|
||||
|
||||
@@ -585,13 +586,13 @@
|
||||
})
|
||||
|
||||
const localModuleStates: Writable<Record<string, GraphModuleState>> = $derived(
|
||||
flowPreviewButtons?.getLocalModuleStates() ?? writable({})
|
||||
flowPreviewContent?.getLocalModuleStates() ?? writable({})
|
||||
)
|
||||
const localDurationStatuses: Writable<Record<string, DurationStatus>> = $derived(
|
||||
flowPreviewButtons?.getLocalDurationStatuses() ?? writable({})
|
||||
flowPreviewContent?.getLocalDurationStatuses() ?? writable({})
|
||||
)
|
||||
const suspendStatus: Writable<Record<string, { job: Job; nb: number }>> = $derived(
|
||||
flowPreviewButtons?.getSuspendStatus() ?? writable({})
|
||||
flowPreviewContent?.getSuspendStatus() ?? writable({})
|
||||
)
|
||||
|
||||
// Create a derived store that only shows the module states when showModuleStatus is true
|
||||
@@ -646,6 +647,8 @@
|
||||
const individualStepTests = $derived(
|
||||
!(showJobStatus && job) && Object.keys($derivedModuleStates).length > 0
|
||||
)
|
||||
|
||||
const flowHasChanged = $derived(flowPreviewContent?.flowHasChanged())
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={onKeyDown} />
|
||||
@@ -800,10 +803,10 @@
|
||||
disableStaticInputs
|
||||
localModuleStates={derivedModuleStates}
|
||||
onTestUpTo={flowPreviewButtons?.testUpTo}
|
||||
isOwner={flowPreviewButtons?.getIsOwner()}
|
||||
isOwner={flowPreviewContent?.getIsOwner?.()}
|
||||
onTestFlow={flowPreviewButtons?.runPreview}
|
||||
isRunning={flowPreviewButtons?.getIsRunning()}
|
||||
onCancelTestFlow={flowPreviewButtons?.cancelTest}
|
||||
isRunning={flowPreviewContent?.getIsRunning?.()}
|
||||
onCancelTestFlow={flowPreviewContent?.cancelTest}
|
||||
onOpenPreview={flowPreviewButtons?.openPreview}
|
||||
onHideJobStatus={resetModulesStates}
|
||||
{individualStepTests}
|
||||
@@ -812,6 +815,7 @@
|
||||
onDelete={(id) => {
|
||||
delete $derivedModuleStates[id]
|
||||
}}
|
||||
{flowHasChanged}
|
||||
/>
|
||||
{:else}
|
||||
<div class="text-red-400 mt-20">Missing flow modules</div>
|
||||
@@ -833,7 +837,7 @@
|
||||
}}
|
||||
onTestFlow={flowPreviewButtons?.runPreview}
|
||||
{job}
|
||||
isOwner={flowPreviewButtons?.getIsOwner()}
|
||||
isOwner={flowPreviewContent?.getIsOwner()}
|
||||
{localDurationStatuses}
|
||||
{suspendStatus}
|
||||
onOpenDetails={flowPreviewButtons?.openPreview}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<script lang="ts">
|
||||
type DiffResult = {
|
||||
added: string[]
|
||||
deleted: string[]
|
||||
modified: string[]
|
||||
repoWmillYaml?: string
|
||||
yamlModified?: boolean
|
||||
}
|
||||
|
||||
let { previewResult } = $props<{
|
||||
previewResult: DiffResult | undefined
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<div class="border rounded p-2 text-xs max-h-40 overflow-y-auto bg-surface-secondary">
|
||||
<div class="font-semibold text-[11px] mb-1 text-tertiary">Preview of changes:</div>
|
||||
{#if !previewResult?.added?.length && !previewResult?.deleted?.length && !previewResult?.modified?.length && !previewResult?.yamlModified}
|
||||
<div class="mt-2 text-tertiary">No changes found! The workspace is up to date.</div>
|
||||
{:else}
|
||||
{#if previewResult?.yamlModified}
|
||||
<div class="mt-2">
|
||||
<div class="text-yellow-600">Modified:</div>
|
||||
<ul class="list-disc list-inside">
|
||||
<li>wmill.yaml (Git sync settings)</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{#if previewResult?.added?.length}
|
||||
<div class="mt-2">
|
||||
<div class="text-green-600">Added:</div>
|
||||
<ul class="list-disc list-inside">
|
||||
{#each previewResult.added as file}
|
||||
<li>
|
||||
{file}{!file.includes('.') ? ' (dir)' : ''}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{#if previewResult?.deleted?.length}
|
||||
<div class="mt-2">
|
||||
<div class="text-red-600">Deleted:</div>
|
||||
<ul class="list-disc list-inside">
|
||||
{#each previewResult.deleted as file}
|
||||
<li>
|
||||
{file}{!file.includes('.') ? ' (dir)' : ''}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{#if previewResult?.modified?.length}
|
||||
<div class="mt-2">
|
||||
<div class="text-yellow-600">Modified:</div>
|
||||
<ul class="list-disc list-inside">
|
||||
{#each previewResult.modified as file}
|
||||
<li>
|
||||
{file}{!file.includes('.') ? ' (dir)' : ''}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,454 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/common'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import {
|
||||
Loader2,
|
||||
Eye,
|
||||
Save,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
UploadCloud,
|
||||
AlertTriangle,
|
||||
Terminal,
|
||||
ChevronDown,
|
||||
ChevronUp
|
||||
} from 'lucide-svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import hubPaths from '$lib/hubPaths.json'
|
||||
import { JobService } from '$lib/gen'
|
||||
import { tryEvery } from '$lib/utils'
|
||||
|
||||
// Types for git sync result
|
||||
interface GitSyncChange {
|
||||
type: 'added' | 'deleted' | 'modified'
|
||||
path: string
|
||||
}
|
||||
|
||||
interface GitSyncResult {
|
||||
changes: GitSyncChange[]
|
||||
}
|
||||
import GitDiffPreview from './GitDiffPreview.svelte'
|
||||
import { page } from '$app/stores'
|
||||
|
||||
let { gitRepoResourcePath, branchName, uiState } = $props<{
|
||||
gitRepoResourcePath: string
|
||||
branchName?: string
|
||||
uiState: {
|
||||
include_path: string[]
|
||||
exclude_path: string[]
|
||||
extra_include_path: string[]
|
||||
include_type: string[]
|
||||
}
|
||||
}>()
|
||||
|
||||
let _branchName = $state(branchName ?? '')
|
||||
let previewResult = $state<
|
||||
| {
|
||||
added: string[]
|
||||
deleted: string[]
|
||||
modified: string[]
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
let isPreviewLoading = $state(false)
|
||||
let isInitializing = $state(false)
|
||||
let initResult = $state<{ success: boolean; message: string | undefined } | null>(null)
|
||||
let initGitRepoPopover = $state<{ open: () => void; close: () => void } | null>(null)
|
||||
let previewJobId = $state<string | null>(null)
|
||||
let previewJobStatus = $state<'running' | 'success' | 'failure' | undefined>(undefined)
|
||||
let pushJobId = $state<string | null>(null)
|
||||
let pushJobStatus = $state<'running' | 'success' | 'failure' | undefined>(undefined)
|
||||
let isCliInfoExpanded = $state(false)
|
||||
|
||||
async function previewChanges() {
|
||||
console.log('Previewing changes for repo:', gitRepoResourcePath)
|
||||
isPreviewLoading = true
|
||||
previewJobId = null
|
||||
previewJobStatus = undefined
|
||||
try {
|
||||
const workspace = $workspaceStore
|
||||
if (!workspace) {
|
||||
previewResult = undefined
|
||||
isPreviewLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
// Pass UI state directly as JSON to CLI
|
||||
const payloadObj = {
|
||||
workspace_id: workspace,
|
||||
repo_url_resource_path: gitRepoResourcePath,
|
||||
branch_to_push: _branchName,
|
||||
dry_run: true,
|
||||
settings_json: JSON.stringify(uiState)
|
||||
}
|
||||
|
||||
const jobId = await JobService.runScriptByPath({
|
||||
workspace,
|
||||
path: hubPaths.gitInitRepo,
|
||||
requestBody: payloadObj,
|
||||
skipPreprocessor: true
|
||||
})
|
||||
previewJobId = jobId
|
||||
previewJobStatus = 'running'
|
||||
|
||||
// Wait for job completion (polling)
|
||||
let jobSuccess = false
|
||||
await tryEvery({
|
||||
tryCode: async () => {
|
||||
const testResult = await JobService.getCompletedJob({
|
||||
workspace,
|
||||
id: jobId
|
||||
})
|
||||
jobSuccess = !!testResult.success
|
||||
},
|
||||
timeoutCode: async () => {
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace,
|
||||
id: jobId,
|
||||
requestBody: {
|
||||
reason: 'Preview job timed out after 15s'
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
},
|
||||
interval: 500,
|
||||
timeout: 15000
|
||||
})
|
||||
|
||||
if (jobSuccess) {
|
||||
const result = await JobService.getCompletedJobResult({
|
||||
workspace,
|
||||
id: jobId
|
||||
})
|
||||
console.log('Preview result:', result)
|
||||
|
||||
// Convert new CLI format to expected format
|
||||
const added: string[] = []
|
||||
const deleted: string[] = []
|
||||
const modified: string[] = []
|
||||
|
||||
if (
|
||||
result &&
|
||||
(result as GitSyncResult).changes &&
|
||||
Array.isArray((result as GitSyncResult).changes)
|
||||
) {
|
||||
for (const change of (result as GitSyncResult).changes) {
|
||||
if (change.type === 'added') {
|
||||
added.push(change.path)
|
||||
} else if (change.type === 'deleted') {
|
||||
deleted.push(change.path)
|
||||
} else if (change.type === 'modified') {
|
||||
modified.push(change.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
previewResult = { added, deleted, modified }
|
||||
previewJobStatus = 'success'
|
||||
} else {
|
||||
previewResult = undefined
|
||||
previewJobStatus = 'failure'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to preview changes:', error)
|
||||
previewResult = undefined
|
||||
previewJobStatus = 'failure'
|
||||
} finally {
|
||||
isPreviewLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeRepo() {
|
||||
const workspace = $workspaceStore
|
||||
if (!workspace) return
|
||||
console.log('Initializing repo:', gitRepoResourcePath, 'in workspace:', workspace)
|
||||
isInitializing = true
|
||||
initResult = null
|
||||
pushJobId = null
|
||||
pushJobStatus = undefined
|
||||
try {
|
||||
// Pass UI state directly as JSON to CLI
|
||||
const jobId = await JobService.runScriptByPath({
|
||||
workspace,
|
||||
path: hubPaths.gitInitRepo,
|
||||
requestBody: {
|
||||
workspace_id: workspace,
|
||||
repo_url_resource_path: gitRepoResourcePath,
|
||||
branch_to_push: _branchName,
|
||||
settings_json: JSON.stringify(uiState)
|
||||
},
|
||||
skipPreprocessor: true
|
||||
})
|
||||
pushJobId = jobId
|
||||
pushJobStatus = 'running'
|
||||
|
||||
let jobSuccess = false
|
||||
await tryEvery({
|
||||
tryCode: async () => {
|
||||
const testResult = await JobService.getCompletedJob({
|
||||
workspace,
|
||||
id: jobId
|
||||
})
|
||||
jobSuccess = !!testResult.success
|
||||
},
|
||||
timeoutCode: async () => {
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace,
|
||||
id: jobId,
|
||||
requestBody: {
|
||||
reason: 'Push job timed out after 5s'
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
},
|
||||
interval: 500,
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
pushJobStatus = jobSuccess ? 'success' : 'failure'
|
||||
initResult = {
|
||||
success: jobSuccess,
|
||||
message: jobSuccess ? undefined : 'Failed to initialize repository.'
|
||||
}
|
||||
|
||||
// Reset popover state after successful push
|
||||
if (jobSuccess) {
|
||||
setTimeout(() => {
|
||||
previewResult = undefined
|
||||
pushJobId = null
|
||||
pushJobStatus = undefined
|
||||
initResult = null
|
||||
initGitRepoPopover?.close()
|
||||
}, 1500) // Small delay to show success state
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize repo:', error)
|
||||
pushJobStatus = 'failure'
|
||||
initResult = { success: false, message: 'Failed to initialize repository.' }
|
||||
} finally {
|
||||
isInitializing = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Popover
|
||||
bind:this={initGitRepoPopover}
|
||||
floatingConfig={{
|
||||
placement: 'top-start',
|
||||
strategy: 'fixed',
|
||||
flip: false,
|
||||
shift: true
|
||||
}}
|
||||
contentClasses="p-4 w-1/3"
|
||||
>
|
||||
<svelte:fragment slot="trigger">
|
||||
<Button
|
||||
color="dark"
|
||||
size="sm"
|
||||
nonCaptureEvent
|
||||
onclick={initGitRepoPopover?.open}
|
||||
startIcon={{ icon: UploadCloud }}
|
||||
>
|
||||
Push workspace to Git repo
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content" let:close>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="text-lg font-semibold">Push workspace to Git repository</h3>
|
||||
<div class="prose max-w-none text-2xs text-tertiary">
|
||||
This action will push all workspace objects that match your current filter settings to the
|
||||
selected branch in your Git repository. <span
|
||||
class="text-orange-600 flex items-center gap-1"
|
||||
><AlertTriangle size={14} /> Any existing content in the branch will be replaced with the
|
||||
filtered workspace content.</span
|
||||
>
|
||||
|
||||
<!-- Collapsible CLI Info Section -->
|
||||
<div class="mt-2 border rounded-md">
|
||||
<button
|
||||
class="w-full flex items-center justify-between p-1.5 bg-surface-secondary hover:bg-surface-hover"
|
||||
onclick={() => (isCliInfoExpanded = !isCliInfoExpanded)}
|
||||
>
|
||||
<span class="font-medium flex items-center gap-2">
|
||||
<Terminal size={14} />
|
||||
Windmill CLI to pull from Windmill and push to git
|
||||
</span>
|
||||
{#if isCliInfoExpanded}
|
||||
<ChevronUp size={16} />
|
||||
{:else}
|
||||
<ChevronDown size={16} />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if isCliInfoExpanded}
|
||||
<div class="p-1 bg-surface-tertiary">
|
||||
<div class="text-2xs mb-2">
|
||||
Not familiar with Windmill CLI? <a
|
||||
href="https://www.windmill.dev/docs/advanced/cli/sync"
|
||||
class="text-blue-500 hover:text-blue-600 underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">Check out the docs</a
|
||||
>
|
||||
</div>
|
||||
<div class="font-mono text-2xs">
|
||||
<pre class="overflow-auto max-h-60"
|
||||
><code
|
||||
>npm install -g windmill-cli
|
||||
wmill workspace add {$workspaceStore} {$workspaceStore} {`${$page.url.protocol}//${$page.url.hostname}/`}
|
||||
wmill init
|
||||
# adjust wmill.yaml file configuraton as needed
|
||||
wmill sync pull
|
||||
git add -A
|
||||
git commit -m 'Initial commit'
|
||||
git push</code
|
||||
></pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="branch-name" class="text-sm font-medium">Push to new branch (optional)</label>
|
||||
<div class="prose max-w-none text-2xs text-tertiary">
|
||||
Enter a new branch name to push to (e.g so you can merge back into main with a pull
|
||||
request). If left blank, the default branch from the git repository resource will be used.
|
||||
</div>
|
||||
<div class="flex flex-col w-1/4">
|
||||
<input
|
||||
id="branch-name"
|
||||
type="text"
|
||||
bind:value={_branchName}
|
||||
class="border rounded px-2 py-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if previewResult}
|
||||
<GitDiffPreview {previewResult} />
|
||||
{/if}
|
||||
|
||||
{#if previewJobId}
|
||||
<div class="flex items-center gap-2 text-xs text-tertiary">
|
||||
{#if previewJobStatus === 'running'}
|
||||
<Loader2 class="animate-spin" size={14} />
|
||||
{:else if previewJobStatus === 'success'}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else if previewJobStatus === 'failure'}
|
||||
<XCircle size={14} class="text-red-700" />
|
||||
{/if}
|
||||
Preview job:
|
||||
<a
|
||||
target="_blank"
|
||||
class="underline"
|
||||
href={`/run/${previewJobId}?workspace=${$workspaceStore}`}
|
||||
>
|
||||
{previewJobId}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if pushJobId}
|
||||
<div class="flex items-center gap-2 text-xs text-tertiary">
|
||||
{#if pushJobStatus === 'running'}
|
||||
<Loader2 class="animate-spin" size={14} />
|
||||
{:else if pushJobStatus === 'success'}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else if pushJobStatus === 'failure'}
|
||||
<XCircle size={14} class="text-red-700" />
|
||||
{/if}
|
||||
Push job:
|
||||
<a
|
||||
target="_blank"
|
||||
class="underline"
|
||||
href={`/run/${pushJobId}?workspace=${$workspaceStore}`}
|
||||
>
|
||||
{pushJobId}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Action row: Cancel on left, Preview/Confirm on right -->
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
previewResult = undefined
|
||||
previewJobId = null
|
||||
previewJobStatus = undefined
|
||||
pushJobId = null
|
||||
pushJobStatus = undefined
|
||||
initResult = null
|
||||
close()
|
||||
}}
|
||||
disabled={isPreviewLoading || isInitializing}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<div class="flex gap-2">
|
||||
{#if !previewResult}
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={previewChanges}
|
||||
disabled={isPreviewLoading || isInitializing}
|
||||
startIcon={{
|
||||
icon: isPreviewLoading ? Loader2 : Eye,
|
||||
classes: isPreviewLoading ? 'animate-spin' : ''
|
||||
}}
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={previewChanges}
|
||||
disabled={isPreviewLoading || isInitializing}
|
||||
startIcon={{
|
||||
icon: isPreviewLoading ? Loader2 : Eye,
|
||||
classes: isPreviewLoading ? 'animate-spin' : ''
|
||||
}}
|
||||
title="Preview changes again"
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
{#if previewResult.added?.length || previewResult.deleted?.length || previewResult.modified?.length}
|
||||
<Button
|
||||
color="red"
|
||||
size="xs"
|
||||
on:click={initializeRepo}
|
||||
disabled={isPreviewLoading || isInitializing}
|
||||
startIcon={{ icon: Save }}
|
||||
title="Initialize Git Repo"
|
||||
>
|
||||
Push
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if initResult?.message}
|
||||
<div class="mt-2">
|
||||
<Alert
|
||||
type={initResult.success ? 'success' : 'error'}
|
||||
title={initResult.success ? 'Success' : 'Error'}
|
||||
size="xs"
|
||||
>
|
||||
{initResult.message}
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
@@ -227,9 +227,17 @@
|
||||
oauths['snowflake_oauth'].connect_config = connect_config
|
||||
}
|
||||
|
||||
let sendingStats = $state(false)
|
||||
async function sendStats() {
|
||||
await SettingService.sendStats()
|
||||
sendUserToast('Usage sent')
|
||||
try {
|
||||
sendingStats = true
|
||||
await SettingService.sendStats()
|
||||
sendUserToast('Usage sent')
|
||||
} catch (err) {
|
||||
throw err
|
||||
} finally {
|
||||
sendingStats = false
|
||||
}
|
||||
}
|
||||
|
||||
function isValidTeamsChannel(value: any): value is TeamsChannel {
|
||||
@@ -332,8 +340,11 @@
|
||||
color="light"
|
||||
btnClasses="w-auto"
|
||||
wrapperClasses="mb-4"
|
||||
size="xs">Send usage</Button
|
||||
loading={sendingStats}
|
||||
size="xs"
|
||||
>
|
||||
Send usage
|
||||
</Button>
|
||||
{/if}
|
||||
{:else if category == 'Auth/OAuth/SAML'}
|
||||
<AuthSettings
|
||||
|
||||
@@ -1,40 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { ResourceService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { getContext, untrack } from 'svelte'
|
||||
import DarkModeObserver from './DarkModeObserver.svelte'
|
||||
import { Button, Drawer, DrawerContent } from './common'
|
||||
import { Plus, Loader2, Link2Off } from 'lucide-svelte'
|
||||
import type { AppViewerContext } from './apps/types'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
|
||||
import Select from './select/Select.svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
|
||||
interface Props {
|
||||
value: string | undefined
|
||||
resourceType?: string | undefined
|
||||
disablePortal?: boolean
|
||||
expressOAuthSetup?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export let initialValue: string | undefined = undefined
|
||||
export let value: string | undefined = initialValue
|
||||
export let resourceType: string | undefined = undefined
|
||||
export let disablePortal = false
|
||||
export let expressOAuthSetup = false
|
||||
export let disabled = false
|
||||
let {
|
||||
value = $bindable(),
|
||||
resourceType = undefined,
|
||||
disablePortal = false,
|
||||
expressOAuthSetup = false,
|
||||
disabled = false
|
||||
}: Props = $props()
|
||||
|
||||
let open = false
|
||||
let refreshCount = 0
|
||||
let open = $state(false)
|
||||
let refreshCount = $state(0)
|
||||
const appViewerContext = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let valueSelect =
|
||||
initialValue || value
|
||||
? {
|
||||
value: value ?? initialValue,
|
||||
label: value ?? initialValue
|
||||
}
|
||||
: undefined
|
||||
let collection = $state(value ? [{ value, label: value }] : [])
|
||||
|
||||
let collection = valueSelect ? [valueSelect] : []
|
||||
|
||||
let loading = true
|
||||
let loading = $state(true)
|
||||
async function loadResources(resourceType: string | undefined) {
|
||||
loading = true
|
||||
try {
|
||||
@@ -49,26 +46,25 @@
|
||||
}))
|
||||
|
||||
// TODO check if this is needed
|
||||
if (!nc.find((x) => x.value == value) && (initialValue || value)) {
|
||||
nc.push({ value: value ?? initialValue!, label: value ?? initialValue! })
|
||||
if (!nc.find((x) => x.value == value) && value) {
|
||||
nc.push({ value: value, label: value })
|
||||
}
|
||||
collection = nc
|
||||
if (expressOAuthSetup && nc.length > 0) {
|
||||
value = nc[0].value
|
||||
valueSelect = nc[0]
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
$: $workspaceStore && loadResources(resourceType)
|
||||
$effect(() => {
|
||||
$workspaceStore && resourceType && untrack(() => loadResources(resourceType))
|
||||
})
|
||||
|
||||
$: dispatchIfMounted('change', value)
|
||||
let darkMode: boolean = $state(false)
|
||||
|
||||
let darkMode: boolean = false
|
||||
|
||||
let drawer: Drawer | undefined = undefined
|
||||
let drawer: Drawer | undefined = $state(undefined)
|
||||
|
||||
export function askNewResource() {
|
||||
refreshCount += 1
|
||||
@@ -93,7 +89,6 @@
|
||||
on:refresh={async (e) => {
|
||||
await loadResources(resourceType)
|
||||
value = e.detail
|
||||
valueSelect = { value, label: value }
|
||||
open = false
|
||||
}}
|
||||
/>
|
||||
@@ -121,7 +116,6 @@
|
||||
on:refresh={async (e) => {
|
||||
await loadResources(resourceType)
|
||||
value = e.detail
|
||||
valueSelect = { value, label: value }
|
||||
drawer?.closeDrawer?.()
|
||||
open = false
|
||||
}}
|
||||
@@ -157,7 +151,6 @@
|
||||
const item = collection[0]
|
||||
if (item) {
|
||||
value = item.value
|
||||
valueSelect = item
|
||||
}
|
||||
}}>Use existing</Button
|
||||
>
|
||||
@@ -167,15 +160,13 @@
|
||||
{disabled}
|
||||
{disablePortal}
|
||||
bind:value={
|
||||
() => valueSelect?.value,
|
||||
() => value,
|
||||
(v) => {
|
||||
value = v
|
||||
valueSelect = collection.find((x) => x.value === v)
|
||||
}
|
||||
}
|
||||
onClear={() => {
|
||||
value = undefined
|
||||
valueSelect = undefined
|
||||
}}
|
||||
clearable
|
||||
items={collection}
|
||||
@@ -184,7 +175,7 @@
|
||||
/>
|
||||
{/if}
|
||||
<div class="flex gap-1 items-center">
|
||||
{#if valueSelect && expressOAuthSetup}
|
||||
{#if value && expressOAuthSetup}
|
||||
<Button
|
||||
{disabled}
|
||||
color="light"
|
||||
@@ -193,15 +184,13 @@
|
||||
btnClasses="w-8 px-0.5 py-1.5"
|
||||
iconOnly
|
||||
on:click={async () => {
|
||||
if (valueSelect && valueSelect.label) {
|
||||
value = undefined
|
||||
if (value) {
|
||||
await ResourceService.deleteResource({
|
||||
workspace: appViewerContext?.workspace ?? $workspaceStore,
|
||||
path: valueSelect.label
|
||||
path: value
|
||||
})
|
||||
await loadResources(resourceType)
|
||||
value = undefined
|
||||
valueSelect = undefined
|
||||
await loadResources(resourceType)
|
||||
}
|
||||
}}
|
||||
startIcon={{ icon: Link2Off }}
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/common'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import {
|
||||
Loader2,
|
||||
Eye,
|
||||
Save,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
DownloadCloud,
|
||||
AlertTriangle,
|
||||
Terminal,
|
||||
ChevronDown,
|
||||
ChevronUp
|
||||
} from 'lucide-svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import hubPaths from '$lib/hubPaths.json'
|
||||
import { JobService } from '$lib/gen'
|
||||
import { tryEvery } from '$lib/utils'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import GitDiffPreview from './GitDiffPreview.svelte'
|
||||
import { page } from '$app/stores'
|
||||
|
||||
// Types for git sync result
|
||||
interface GitSyncChange {
|
||||
type: 'added' | 'deleted' | 'modified'
|
||||
path: string
|
||||
}
|
||||
|
||||
interface GitSyncResult {
|
||||
changes: GitSyncChange[]
|
||||
}
|
||||
|
||||
let { gitRepoResourcePath, uiState, onFilterUpdate } = $props<{
|
||||
gitRepoResourcePath: string
|
||||
uiState: {
|
||||
include_path: string[]
|
||||
exclude_path: string[]
|
||||
extra_include_path: string[]
|
||||
include_type: string[]
|
||||
}
|
||||
onFilterUpdate: (filters: {
|
||||
include_path: string[]
|
||||
exclude_path: string[]
|
||||
extra_include_path: string[]
|
||||
include_type: string[]
|
||||
}) => void
|
||||
}>()
|
||||
|
||||
type PreviewResult = {
|
||||
added: string[]
|
||||
deleted: string[]
|
||||
modified: string[]
|
||||
}
|
||||
|
||||
let previewResult = $state<PreviewResult | undefined>(undefined)
|
||||
|
||||
let isPreviewLoading = $state(false)
|
||||
let isPulling = $state(false)
|
||||
let pullGitRepoPopover = $state<{ open: () => void; close: () => void } | null>(null)
|
||||
let jobStatus = $state<{
|
||||
id: string | null
|
||||
status: 'running' | 'success' | 'failure' | undefined
|
||||
error?: string
|
||||
type: 'preview' | 'pull'
|
||||
}>({
|
||||
id: null,
|
||||
status: undefined,
|
||||
type: 'preview'
|
||||
})
|
||||
|
||||
let isCliInfoExpanded = $state(false)
|
||||
|
||||
async function handleJobCompletion(jobId: string, workspace: string): Promise<boolean> {
|
||||
let success = false
|
||||
await tryEvery({
|
||||
tryCode: async () => {
|
||||
const result = await JobService.getCompletedJob({
|
||||
workspace,
|
||||
id: jobId
|
||||
})
|
||||
success = !!result.success
|
||||
},
|
||||
timeoutCode: async () => {
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace,
|
||||
id: jobId,
|
||||
requestBody: {
|
||||
reason: 'Job timed out after 5s'
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
},
|
||||
interval: 500,
|
||||
timeout: 10000
|
||||
})
|
||||
return success
|
||||
}
|
||||
|
||||
async function previewChanges() {
|
||||
const workspace = $workspaceStore
|
||||
if (!workspace) return
|
||||
|
||||
console.log('Previewing changes for repo:', gitRepoResourcePath)
|
||||
isPreviewLoading = true
|
||||
jobStatus = { id: null, status: undefined, type: 'preview' }
|
||||
|
||||
try {
|
||||
// Always use the simplified JSON approach
|
||||
const jobId = await JobService.runScriptByPath({
|
||||
workspace,
|
||||
path: hubPaths.gitInitRepo,
|
||||
requestBody: {
|
||||
workspace_id: workspace,
|
||||
repo_url_resource_path: gitRepoResourcePath,
|
||||
dry_run: true,
|
||||
pull: true,
|
||||
only_wmill_yaml: false,
|
||||
settings_json: JSON.stringify(uiState)
|
||||
},
|
||||
skipPreprocessor: true
|
||||
})
|
||||
|
||||
jobStatus = { id: jobId, status: 'running', type: 'preview' }
|
||||
const success = await handleJobCompletion(jobId, workspace)
|
||||
|
||||
if (success) {
|
||||
const rawResult = await JobService.getCompletedJobResult({ workspace, id: jobId })
|
||||
console.log('Preview result:', rawResult)
|
||||
|
||||
// Convert new CLI format to expected format
|
||||
const added: string[] = []
|
||||
const deleted: string[] = []
|
||||
const modified: string[] = []
|
||||
|
||||
if (
|
||||
rawResult &&
|
||||
(rawResult as GitSyncResult).changes &&
|
||||
Array.isArray((rawResult as GitSyncResult).changes)
|
||||
) {
|
||||
for (const change of (rawResult as GitSyncResult).changes) {
|
||||
if (change.type === 'added') {
|
||||
added.push(change.path)
|
||||
} else if (change.type === 'deleted') {
|
||||
deleted.push(change.path)
|
||||
} else if (change.type === 'modified') {
|
||||
modified.push(change.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For full sync mode, just use the CLI results directly
|
||||
// The CLI already handles wmill.yaml changes with --include-wmill-yaml flag
|
||||
previewResult = { added, deleted, modified }
|
||||
jobStatus.status = 'success'
|
||||
} else {
|
||||
previewResult = undefined
|
||||
jobStatus.status = 'failure'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to preview changes:', error)
|
||||
previewResult = undefined
|
||||
jobStatus = {
|
||||
...jobStatus,
|
||||
status: 'failure',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
} finally {
|
||||
isPreviewLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function pullFromRepo() {
|
||||
const workspace = $workspaceStore
|
||||
if (!workspace) return
|
||||
|
||||
console.log('Pulling from repo:', gitRepoResourcePath)
|
||||
isPulling = true
|
||||
jobStatus = { id: null, status: undefined, type: 'pull' }
|
||||
|
||||
try {
|
||||
// Use init git repo script with dry_run: false (actual pull operation)
|
||||
// The script will read wmill.yaml directly from the cloned repo, no need to pass settings
|
||||
const jobId = await JobService.runScriptByPath({
|
||||
workspace,
|
||||
path: hubPaths.gitInitRepo,
|
||||
requestBody: {
|
||||
workspace_id: workspace,
|
||||
repo_url_resource_path: gitRepoResourcePath,
|
||||
dry_run: false,
|
||||
branch_to_push: '',
|
||||
only_wmill_yaml: false,
|
||||
pull: true,
|
||||
settings_json: undefined // Let script use wmill.yaml from repo
|
||||
},
|
||||
skipPreprocessor: true
|
||||
})
|
||||
|
||||
jobStatus = { id: jobId, status: 'running', type: 'pull' }
|
||||
const success = await handleJobCompletion(jobId, workspace)
|
||||
jobStatus.status = success ? 'success' : 'failure'
|
||||
|
||||
if (success) {
|
||||
// Get the result which should contain the local git repo settings as JSON
|
||||
const result = (await JobService.getCompletedJobResult({ workspace, id: jobId })) as any
|
||||
console.log('Pull result:', result)
|
||||
|
||||
// Apply the settings from the sync operation result to the UI
|
||||
if (result?.settings_json) {
|
||||
// Directly update the UI state with the JSON result - no YAML conversion needed!
|
||||
const settingsJson = result.settings_json as {
|
||||
include_path: string[]
|
||||
exclude_path?: string[]
|
||||
extra_include_path?: string[]
|
||||
include_type: string[]
|
||||
}
|
||||
onFilterUpdate({
|
||||
include_path: settingsJson.include_path || ['f/**'],
|
||||
exclude_path: settingsJson.exclude_path || [],
|
||||
extra_include_path: settingsJson.extra_include_path || [],
|
||||
include_type: settingsJson.include_type || ['script', 'flow', 'app', 'folder']
|
||||
})
|
||||
sendUserToast('Successfully pulled workspace content from repository')
|
||||
|
||||
// Reset popover state after successful pull
|
||||
previewResult = undefined
|
||||
jobStatus = { id: null, status: undefined, type: 'preview' }
|
||||
pullGitRepoPopover?.close()
|
||||
} else {
|
||||
console.warn('No settings_json returned from pull operation')
|
||||
sendUserToast('Pull completed but could not update filter settings', true)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to pull from repo:', error)
|
||||
jobStatus = {
|
||||
...jobStatus,
|
||||
status: 'failure',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
} finally {
|
||||
isPulling = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Popover
|
||||
bind:this={pullGitRepoPopover}
|
||||
floatingConfig={{
|
||||
placement: 'top-start',
|
||||
strategy: 'fixed',
|
||||
flip: false,
|
||||
shift: true
|
||||
}}
|
||||
contentClasses="p-4 w-1/3"
|
||||
>
|
||||
<svelte:fragment slot="trigger">
|
||||
<Button
|
||||
color="dark"
|
||||
size="sm"
|
||||
nonCaptureEvent
|
||||
onclick={pullGitRepoPopover?.open}
|
||||
startIcon={{ icon: DownloadCloud }}
|
||||
>
|
||||
Pull workspace from Git repo
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="content" let:close>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="text-lg font-semibold">Pull workspace from Git repository</h3>
|
||||
<div class="prose max-w-none text-2xs text-tertiary">
|
||||
This action will pull all workspace objects from your Git repository according to the
|
||||
filters set in the Git repository wmill.yaml file and apply those filter settings to the
|
||||
workspace.
|
||||
<span class="text-orange-600 flex items-center gap-1">
|
||||
<AlertTriangle size={14} /> This will overwrite your current workspace content and Git sync
|
||||
filter settings with the content from the Git repository.
|
||||
</span>
|
||||
|
||||
<!-- Collapsible CLI Info Section -->
|
||||
<div class="mt-2 border rounded-md">
|
||||
<button
|
||||
class="w-full flex items-center justify-between p-1.5 bg-surface-secondary hover:bg-surface-hover"
|
||||
onclick={() => (isCliInfoExpanded = !isCliInfoExpanded)}
|
||||
>
|
||||
<span class="font-medium flex items-center gap-2">
|
||||
<Terminal size={14} />
|
||||
Windmill CLI to push local files to Windmill
|
||||
</span>
|
||||
{#if isCliInfoExpanded}
|
||||
<ChevronUp size={16} />
|
||||
{:else}
|
||||
<ChevronDown size={16} />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if isCliInfoExpanded}
|
||||
<div class="p-1 bg-surface-tertiary">
|
||||
<div class="text-2xs mb-2">
|
||||
Not familiar with Windmill CLI? <a
|
||||
href="https://www.windmill.dev/docs/advanced/cli/sync"
|
||||
class="text-blue-500 hover:text-blue-600 underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">Check out the docs</a
|
||||
>
|
||||
</div>
|
||||
<div class="font-mono text-2xs">
|
||||
<pre class="overflow-auto max-h-60"
|
||||
><code
|
||||
>npm install -g windmill-cli
|
||||
# Clone your git repository
|
||||
git clone $REPO_URL
|
||||
cd $REPO_NAME
|
||||
# Configure Windmill CLI
|
||||
wmill workspace add {$workspaceStore} {$workspaceStore} {`${$page.url.protocol}//${$page.url.hostname}/`}
|
||||
# Push the content to Windmill
|
||||
wmill sync push --yes
|
||||
# Optional: add --skip-secrets --skip-variables --skip-resources flags as needed</code
|
||||
></pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if previewResult}
|
||||
<GitDiffPreview {previewResult} />
|
||||
{/if}
|
||||
|
||||
{#if jobStatus.id}
|
||||
<div class="flex items-center gap-2 text-xs text-tertiary">
|
||||
{#if jobStatus.status === 'running'}
|
||||
<Loader2 class="animate-spin" size={14} />
|
||||
{:else if jobStatus.status === 'success'}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else if jobStatus.status === 'failure'}
|
||||
<XCircle size={14} class="text-red-700" />
|
||||
{/if}
|
||||
{jobStatus.type === 'preview' ? 'Preview' : 'Pull'} job:
|
||||
<a
|
||||
target="_blank"
|
||||
class="underline"
|
||||
href={`/run/${jobStatus.id}?workspace=${$workspaceStore}`}
|
||||
>
|
||||
{jobStatus.id}
|
||||
</a>
|
||||
</div>
|
||||
{#if jobStatus.error}
|
||||
<div class="text-xs text-red-600">{jobStatus.error}</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
previewResult = undefined
|
||||
jobStatus = { id: null, status: undefined, type: 'preview' }
|
||||
close()
|
||||
}}
|
||||
disabled={isPreviewLoading || isPulling}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<div class="flex gap-2">
|
||||
{#if !previewResult}
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={previewChanges}
|
||||
disabled={isPreviewLoading || isPulling}
|
||||
startIcon={{
|
||||
icon: isPreviewLoading ? Loader2 : Eye,
|
||||
classes: isPreviewLoading ? 'animate-spin' : ''
|
||||
}}
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={previewChanges}
|
||||
disabled={isPreviewLoading || isPulling}
|
||||
startIcon={{
|
||||
icon: isPreviewLoading ? Loader2 : Eye,
|
||||
classes: isPreviewLoading ? 'animate-spin' : ''
|
||||
}}
|
||||
title="Preview changes again"
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
{#if previewResult.added?.length || previewResult.deleted?.length || previewResult.modified?.length}
|
||||
<Button
|
||||
color="red"
|
||||
size="xs"
|
||||
on:click={pullFromRepo}
|
||||
disabled={isPreviewLoading || isPulling}
|
||||
startIcon={{
|
||||
icon: isPulling ? Loader2 : Save,
|
||||
classes: isPulling ? 'animate-spin' : ''
|
||||
}}
|
||||
>
|
||||
{isPulling ? 'Pulling...' : 'Pull'}
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
@@ -40,6 +40,7 @@
|
||||
noInitialize?: boolean
|
||||
replaceCallback?: boolean
|
||||
controls?: { left: () => boolean; right: () => boolean | string } | undefined
|
||||
onDone?: () => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -59,7 +60,8 @@
|
||||
isMenuItem = false,
|
||||
noInitialize = false,
|
||||
replaceCallback = false,
|
||||
controls = undefined
|
||||
controls = undefined,
|
||||
onDone = undefined
|
||||
}: Props = $props()
|
||||
|
||||
const { worldStore, app, componentControl, selectedComponent } =
|
||||
@@ -240,6 +242,12 @@
|
||||
listInputs.set(id, inputOutput)
|
||||
}
|
||||
}}
|
||||
on:done={() => {
|
||||
onDone?.()
|
||||
}}
|
||||
on:doneError={() => {
|
||||
onDone?.()
|
||||
}}
|
||||
refreshOnStart={resolvedConfig.triggerOnAppLoad}
|
||||
{replaceCallback}
|
||||
>
|
||||
|
||||
@@ -171,7 +171,6 @@
|
||||
outputs?.result.set({
|
||||
latestButtonClicked: actionButton.id
|
||||
})
|
||||
menu?.close()
|
||||
}}
|
||||
>
|
||||
<AppButton
|
||||
@@ -185,6 +184,9 @@
|
||||
componentInput={actionButton.componentInput}
|
||||
noWFull={false}
|
||||
isMenuItem={true}
|
||||
onDone={() => {
|
||||
menu?.close()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onDestroy } from 'svelte'
|
||||
import { getContext, onDestroy, untrack } from 'svelte'
|
||||
import { initConfig, initOutput } from '../../editor/appUtils'
|
||||
import type {
|
||||
AppViewerContext,
|
||||
@@ -33,7 +33,8 @@
|
||||
render
|
||||
}: Props = $props()
|
||||
|
||||
const { app, worldStore, componentControl } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { app, worldStore, componentControl, isEditor, mode } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let resolvedConfig = $state(
|
||||
initConfig(components['userresourcecomponent'].initialData.configuration, configuration)
|
||||
@@ -57,9 +58,34 @@
|
||||
)
|
||||
)
|
||||
|
||||
let value: string | undefined = $state(outputs.result.peak()?.replace('$res:', ''))
|
||||
function getDefaultValue(): string | undefined {
|
||||
if (resolvedConfig.defaultValue && typeof resolvedConfig.defaultValue === 'string') {
|
||||
const nval = resolvedConfig.defaultValue as string
|
||||
return nval.replace('$res:', '')
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
value && assignValue(outputs.result.peak())
|
||||
let value: string | undefined = $state(
|
||||
outputs.result.peak()?.replace('$res:', '') ?? getDefaultValue()
|
||||
)
|
||||
|
||||
$effect(() => {
|
||||
value
|
||||
untrack(() => assignValue(value))
|
||||
})
|
||||
|
||||
let lastDefaultValue = $state(getDefaultValue())
|
||||
$effect(() => {
|
||||
// when in dnd mode, we react to the default value being changed for better UX
|
||||
if (isEditor && $mode === 'dnd') {
|
||||
const currentDefaultValue = getDefaultValue() // reactive
|
||||
if (lastDefaultValue !== currentDefaultValue) {
|
||||
value = currentDefaultValue
|
||||
lastDefaultValue = currentDefaultValue
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
listInputs?.remove(id)
|
||||
@@ -74,7 +100,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function assignValue(value: string) {
|
||||
function assignValue(value: string | undefined) {
|
||||
let nval
|
||||
if (!value || value === '') {
|
||||
nval = undefined
|
||||
@@ -119,10 +145,7 @@
|
||||
<LightweightResourcePicker
|
||||
expressOAuthSetup={resolvedConfig.expressOauthSetup}
|
||||
bind:this={resourcePicker}
|
||||
{value}
|
||||
on:change={(e) => {
|
||||
assignValue(e.detail)
|
||||
}}
|
||||
bind:value
|
||||
disabled={resolvedConfig.disabled}
|
||||
resourceType={resolvedConfig.resourceType}
|
||||
/>
|
||||
|
||||
@@ -1648,8 +1648,8 @@ export const components = {
|
||||
backgroundColor: ['#FF6384', '#4BC0C0', '#FFCE56']
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
chartjscomponentv2: {
|
||||
@@ -1687,9 +1687,8 @@ export const components = {
|
||||
backgroundColor: ['#FF6384', '#4BC0C0', '#FFCE56']
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
barchartcomponent: {
|
||||
@@ -1859,8 +1858,7 @@ This is a paragraph.
|
||||
width: 2.5
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
},
|
||||
configuration: {
|
||||
layout: {
|
||||
@@ -1893,7 +1891,7 @@ This is a paragraph.
|
||||
width: 2.5
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
configuration: {
|
||||
layout: {
|
||||
@@ -1904,8 +1902,7 @@ This is a paragraph.
|
||||
'Layout options for the plot. See https://plotly.com/javascript/reference/layout/ for more information'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
},
|
||||
timeseriescomponent: {
|
||||
name: 'Timeseries',
|
||||
@@ -2431,6 +2428,12 @@ This is a paragraph.
|
||||
allowTypeChange: false,
|
||||
value: []
|
||||
} as StaticAppInput,
|
||||
defaultValue: {
|
||||
type: 'static',
|
||||
fieldType: 'text',
|
||||
value: undefined,
|
||||
tooltip: 'Format: $res:path/to/resource'
|
||||
},
|
||||
placeholder: {
|
||||
type: 'static',
|
||||
fieldType: 'text',
|
||||
@@ -2468,6 +2471,12 @@ This is a paragraph.
|
||||
fieldType: 'text',
|
||||
value: 'postgresql'
|
||||
},
|
||||
defaultValue: {
|
||||
type: 'static',
|
||||
fieldType: 'text',
|
||||
value: undefined,
|
||||
tooltip: 'Format: $res:path/to/resource'
|
||||
},
|
||||
expressOauthSetup: {
|
||||
type: 'static',
|
||||
fieldType: 'boolean',
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { AppService, HelpersService } from '$lib/gen'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import { Ban, CheckCheck, FileWarning, Files, RefreshCcw, Trash } from 'lucide-svelte'
|
||||
import { Ban, CheckCheck, FileWarning, Files, RefreshCcw, Trash, XIcon } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { emptyString } from '$lib/utils'
|
||||
@@ -94,16 +94,15 @@
|
||||
)
|
||||
init()
|
||||
|
||||
function transform(initialValue: { s3: string; filename?: string }) {
|
||||
return {
|
||||
name: initialValue?.filename ?? initialValue?.s3 ?? 'Unknown file',
|
||||
path: initialValue?.s3,
|
||||
fromBucket: true
|
||||
}
|
||||
}
|
||||
function init() {
|
||||
if (initialS3) {
|
||||
function transform(initialValue: { s3: string; filename: string }) {
|
||||
return {
|
||||
name: initialValue.filename,
|
||||
size: 0,
|
||||
progress: 100,
|
||||
path: initialValue?.s3
|
||||
}
|
||||
}
|
||||
for (const s3 of initialS3) {
|
||||
if (!$fileUploads.find((fileUpload) => fileUpload.path === s3)) {
|
||||
let initialFileUploads = initialValue
|
||||
@@ -117,15 +116,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
export function addUpload(upload: { s3: string; filename?: string }) {
|
||||
$fileUploads = [...$fileUploads, transform(upload)]
|
||||
}
|
||||
|
||||
export function setUpload(upload: { s3: string; filename?: string }) {
|
||||
$fileUploads = [transform(upload)]
|
||||
}
|
||||
|
||||
type FileUploadData = {
|
||||
name: string
|
||||
size: number
|
||||
progress: number
|
||||
size?: number
|
||||
progress?: number
|
||||
cancelled?: boolean
|
||||
errorMessage?: string
|
||||
path?: string
|
||||
file?: File
|
||||
deleteToken?: string
|
||||
fromBucket?: boolean
|
||||
}
|
||||
|
||||
async function handleChange(files: File[] | undefined) {
|
||||
@@ -394,8 +402,28 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
{#snippet fileInput()}
|
||||
<FileInput
|
||||
{folderOnly}
|
||||
{disabled}
|
||||
accept={acceptedFileTypes?.join(',')}
|
||||
multiple={allowMultiple}
|
||||
returnFileNames
|
||||
{iconSize}
|
||||
on:change={({ detail }) => {
|
||||
forceDisplayUploads = false
|
||||
handleChange(detail)
|
||||
}}
|
||||
class={twMerge('w-full h-full', customClass, 'wm-file-input')}
|
||||
style={customStyle}
|
||||
defaultFile={defaultValue}
|
||||
>
|
||||
{containerText}{#if disabled}<br />(Disabled){/if}
|
||||
</FileInput>
|
||||
{/snippet}
|
||||
|
||||
<div class="w-full h-full p-0 flex flex-col">
|
||||
{#if $fileUploads.length > 0 && !forceDisplayUploads}
|
||||
{#if $fileUploads.length > 0}
|
||||
<div class="border rounded-md flex flex-col gap-1 divide-y h-full w-full p-1">
|
||||
<div class="flex h-full overflow-y-auto flex-col">
|
||||
{#each $fileUploads as fileUpload}
|
||||
@@ -468,7 +496,7 @@
|
||||
Remove from list
|
||||
</Button>
|
||||
{/if}
|
||||
{#if fileUpload.progress < 100 && !fileUpload.cancelled && !fileUpload.errorMessage}
|
||||
{#if fileUpload.progress !== undefined && fileUpload.progress < 100 && !fileUpload.cancelled && !fileUpload.errorMessage}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
@@ -506,100 +534,115 @@
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
{:else if fileUpload.fromBucket}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="red"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
$fileUploads = $fileUploads.filter(
|
||||
(_fileUpload) => _fileUpload.name !== fileUpload.name
|
||||
)
|
||||
|
||||
if (fileUpload.path) {
|
||||
dispatch('deletion', { path: fileUpload.path })
|
||||
}
|
||||
}}
|
||||
startIcon={{
|
||||
icon: XIcon
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<FileProgressBar
|
||||
progress={fileUpload.progress}
|
||||
color={fileUpload.errorMessage
|
||||
? '#ef4444'
|
||||
: fileUpload.cancelled
|
||||
? '#eab308'
|
||||
: fileUpload.progress === 100
|
||||
? '#22c55e'
|
||||
: '#3b82f6'}
|
||||
ended={fileUpload.cancelled || fileUpload.errorMessage !== undefined}
|
||||
>
|
||||
{#if fileUpload.errorMessage}
|
||||
<span class="text-xs text-red-600">{fileUpload.errorMessage}</span>
|
||||
{:else if fileUpload.cancelled}
|
||||
<span class="text-xs text-yellow-600">Upload cancelled</span>
|
||||
{/if}
|
||||
</FileProgressBar>
|
||||
{#if !fileUpload.fromBucket}
|
||||
<FileProgressBar
|
||||
progress={fileUpload.progress}
|
||||
color={fileUpload.errorMessage
|
||||
? '#ef4444'
|
||||
: fileUpload.cancelled
|
||||
? '#eab308'
|
||||
: fileUpload.progress === 100
|
||||
? '#22c55e'
|
||||
: '#3b82f6'}
|
||||
ended={fileUpload.cancelled || fileUpload.errorMessage !== undefined}
|
||||
>
|
||||
{#if fileUpload.errorMessage}
|
||||
<span class="text-xs text-red-600">{fileUpload.errorMessage}</span>
|
||||
{:else if fileUpload.cancelled}
|
||||
<span class="text-xs text-yellow-600">Upload cancelled</span>
|
||||
{/if}
|
||||
</FileProgressBar>
|
||||
{/if}
|
||||
{#if !(fileUpload.cancelled || fileUpload.errorMessage !== undefined)}
|
||||
<span class="text-xs text-gray-500 dark:text-gray-200">
|
||||
{fileUpload.progress === 100 ? 'Upload finished' : `Uploading`} to path: {fileUpload.path ??
|
||||
'N/A'}
|
||||
{#if fileUpload.fromBucket}
|
||||
{fileUpload.path ?? 'N/A'}
|
||||
{:else}
|
||||
{fileUpload.progress === 100 ? 'Upload finished' : `Uploading`} to path: {fileUpload.path ??
|
||||
'N/A'}
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if allowMultiple}
|
||||
<div class="flex flex-row gap-1 items-center justify-end p-1">
|
||||
{#if !$fileUploads.every((fileUpload) => fileUpload.progress === 100 || fileUpload.cancelled)}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
$fileUploads = $fileUploads.map((fileUpload) => {
|
||||
if (fileUpload.progress === 100 || fileUpload.cancelled) {
|
||||
return fileUpload
|
||||
}
|
||||
|
||||
fileUpload.cancelled = true
|
||||
fileUpload.progress = 0
|
||||
return fileUpload
|
||||
})
|
||||
}}
|
||||
startIcon={{
|
||||
icon: Ban
|
||||
}}
|
||||
>
|
||||
Cancel all uploads
|
||||
</Button>
|
||||
{/if}
|
||||
<div class="flex flex-row gap-1 items-center justify-end p-1">
|
||||
{#if !forceDisplayUploads && (allowMultiple || folderOnly) && !$fileUploads.every((fileUpload) => fileUpload.progress === 100 || fileUpload.cancelled || fileUpload.fromBucket)}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
forceDisplayUploads = true
|
||||
$fileUploads = $fileUploads.map((fileUpload) => {
|
||||
if (fileUpload.progress === 100 || fileUpload.cancelled) {
|
||||
return fileUpload
|
||||
}
|
||||
|
||||
fileUpload.cancelled = true
|
||||
fileUpload.progress = 0
|
||||
return fileUpload
|
||||
})
|
||||
}}
|
||||
startIcon={{
|
||||
icon: Files
|
||||
icon: Ban
|
||||
}}
|
||||
disabled={$fileUploads.some(
|
||||
(fileUpload) => fileUpload.progress !== 100 && !fileUpload.cancelled
|
||||
)}
|
||||
>
|
||||
Upload more files
|
||||
Cancel all uploads
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if allowMultiple}
|
||||
{#if forceDisplayUploads}
|
||||
{@render fileInput()}
|
||||
{:else}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
forceDisplayUploads = true
|
||||
}}
|
||||
startIcon={{
|
||||
icon: Files
|
||||
}}
|
||||
disabled={$fileUploads.some(
|
||||
(fileUpload) =>
|
||||
fileUpload.progress !== 100 && !fileUpload.cancelled && !fileUpload.fromBucket
|
||||
)}
|
||||
>
|
||||
Upload more files
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<FileInput
|
||||
{folderOnly}
|
||||
{disabled}
|
||||
accept={acceptedFileTypes?.join(',')}
|
||||
multiple={allowMultiple}
|
||||
returnFileNames
|
||||
{iconSize}
|
||||
on:change={({ detail }) => {
|
||||
forceDisplayUploads = false
|
||||
handleChange(detail)
|
||||
}}
|
||||
class={twMerge('w-full h-full', customClass, 'wm-file-input')}
|
||||
style={customStyle}
|
||||
defaultFile={defaultValue}
|
||||
>
|
||||
{containerText}{#if disabled}<br />(Disabled){/if}
|
||||
</FileInput>
|
||||
{@render fileInput()}
|
||||
{/if}
|
||||
{#if initialS3 && initialS3.length > 0 && $fileUploads.length == 0}
|
||||
<div class="flex flex-row gap-1 items-center p-1">
|
||||
<span class="text-sm">
|
||||
File currently selected: {initialS3?.join(', ')}
|
||||
File{initialS3.length > 1 ? 's' : ''} currently selected: {initialS3?.join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<script lang="ts">
|
||||
import S3FilePicker from '$lib/components/S3FilePicker.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { Loader2, Pipette } from 'lucide-svelte'
|
||||
import FileUpload from './FileUpload.svelte'
|
||||
import type SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import Button from '../button/Button.svelte'
|
||||
|
||||
let {
|
||||
multiple,
|
||||
value = $bindable(),
|
||||
defaultValue,
|
||||
setNewValueFromCode,
|
||||
onFocus,
|
||||
onBlur,
|
||||
computeS3ForceViewerPolicies,
|
||||
workspace,
|
||||
editor = $bindable(),
|
||||
appPath
|
||||
}: {
|
||||
multiple: boolean
|
||||
value: any
|
||||
defaultValue: any
|
||||
setNewValueFromCode: (v: any) => void
|
||||
onFocus: () => void
|
||||
onBlur: () => void
|
||||
computeS3ForceViewerPolicies:
|
||||
| (() =>
|
||||
| {
|
||||
allowed_resources: string[]
|
||||
allow_user_resources: boolean
|
||||
allow_workspace_resource: boolean
|
||||
file_key_regex: string
|
||||
}
|
||||
| undefined)
|
||||
| undefined
|
||||
workspace: string | undefined
|
||||
editor: SimpleEditor | undefined
|
||||
appPath: string | undefined
|
||||
} = $props()
|
||||
|
||||
let s3FileUploadRawMode: boolean = $state(false)
|
||||
|
||||
let s3FilePicker: S3FilePicker | undefined = $state(undefined)
|
||||
let fileUpload: FileUpload | undefined = $state(undefined)
|
||||
</script>
|
||||
|
||||
<S3FilePicker
|
||||
bind:this={s3FilePicker}
|
||||
on:selectAndClose={(ev) => {
|
||||
if (multiple) {
|
||||
if (Array.isArray(value)) {
|
||||
value.push(ev.detail)
|
||||
} else {
|
||||
value = [ev.detail]
|
||||
}
|
||||
fileUpload?.addUpload(ev.detail)
|
||||
} else {
|
||||
value = ev.detail
|
||||
fileUpload?.setUpload(value)
|
||||
}
|
||||
editor?.setCode(JSON.stringify(value))
|
||||
}}
|
||||
readOnlyMode={false}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col w-full gap-1">
|
||||
<Toggle
|
||||
class="flex justify-end"
|
||||
bind:checked={s3FileUploadRawMode}
|
||||
size="xs"
|
||||
options={{ left: `Raw S3 object${multiple ? 's' : ''} input` }}
|
||||
/>
|
||||
{#if s3FileUploadRawMode}
|
||||
{#await import('$lib/components/JsonEditor.svelte')}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then Module}
|
||||
<Module.default
|
||||
bind:editor
|
||||
on:focus={(e) => {
|
||||
onFocus()
|
||||
}}
|
||||
on:blur={(e) => {
|
||||
onBlur()
|
||||
}}
|
||||
code={JSON.stringify(value ?? defaultValue ?? (multiple ? [] : { s3: '' }), null, 2)}
|
||||
on:changeValue={(e) => {
|
||||
setNewValueFromCode(e.detail)
|
||||
}}
|
||||
/>
|
||||
{/await}
|
||||
{:else}
|
||||
<FileUpload
|
||||
bind:this={fileUpload}
|
||||
{appPath}
|
||||
computeForceViewerPolicies={computeS3ForceViewerPolicies}
|
||||
{workspace}
|
||||
allowMultiple={multiple}
|
||||
randomFileKey={true}
|
||||
on:addition={(evt) => {
|
||||
const s3Object = {
|
||||
s3: evt.detail?.path ?? '',
|
||||
filename: evt.detail?.filename ?? ''
|
||||
}
|
||||
if (multiple) {
|
||||
if (Array.isArray(value)) {
|
||||
value.push(s3Object)
|
||||
} else {
|
||||
value = [s3Object]
|
||||
}
|
||||
} else {
|
||||
value = s3Object
|
||||
}
|
||||
}}
|
||||
on:deletion={(evt) => {
|
||||
if (multiple) {
|
||||
if (Array.isArray(value)) {
|
||||
value = value.filter((v) => v.s3 !== evt.detail?.path)
|
||||
}
|
||||
} else {
|
||||
value = {
|
||||
s3: ''
|
||||
}
|
||||
}
|
||||
}}
|
||||
defaultValue={defaultValue?.s3}
|
||||
initialValue={value}
|
||||
/>
|
||||
{/if}
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
btnClasses="mt-1"
|
||||
on:click={() => {
|
||||
s3FilePicker?.open?.(value)
|
||||
}}
|
||||
startIcon={{ icon: Pipette }}
|
||||
>
|
||||
{multiple ? 'Add' : 'Choose'} an object from the catalog
|
||||
</Button>
|
||||
</div>
|
||||
@@ -502,7 +502,6 @@
|
||||
disabled={!can_write}
|
||||
/>
|
||||
{/if}
|
||||
{s3FileUploadRawMode}
|
||||
{#if s3FileUploadRawMode}
|
||||
{#if can_write}
|
||||
<JsonEditor
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import { Plus, X } from 'lucide-svelte'
|
||||
|
||||
let {
|
||||
title = '',
|
||||
tooltip = undefined,
|
||||
items = $bindable([] as string[]),
|
||||
placeholder = 'Add filter (e.g. f/**)'
|
||||
} = $props()
|
||||
|
||||
let newItem = $state('')
|
||||
let inputRef: HTMLInputElement | null = $state(null)
|
||||
|
||||
function addItem() {
|
||||
const value = newItem.trim()
|
||||
if (value && !items.includes(value)) {
|
||||
items = [...items, value]
|
||||
newItem = ''
|
||||
inputRef?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
function removeItem(idx: number) {
|
||||
items = items.filter((_, i) => i !== idx)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<h4 class="font-semibold text-sm">{title}</h4>
|
||||
{#if tooltip}
|
||||
{@render tooltip?.()}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 items-center mb-1">
|
||||
{#each items as item, idx (item)}
|
||||
<span class="flex items-center bg-gray-100 rounded-full px-3 py-1 text-xs text-gray-700">
|
||||
{item}
|
||||
<button
|
||||
class="ml-2 text-gray-400 hover:text-red-500 focus:outline-none"
|
||||
onclick={() => removeItem(idx)}
|
||||
aria-label="Remove filter"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</span>
|
||||
{/each}
|
||||
|
||||
<input
|
||||
bind:this={inputRef}
|
||||
class="border border-gray-300 rounded-full px-3 py-1 text-xs focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
{placeholder}
|
||||
value={newItem}
|
||||
oninput={(e) => (newItem = e.currentTarget.value)}
|
||||
onkeydown={(e) => e.key === 'Enter' && (addItem(), e.preventDefault())}
|
||||
/>
|
||||
<button
|
||||
class="ml-1 text-primary hover:bg-primary/10 rounded-full p-1"
|
||||
onclick={addItem}
|
||||
aria-label="Add filter"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,877 @@
|
||||
<script lang="ts">
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { Filter, Save, Eye, Loader2, CheckCircle2, XCircle, Check } from 'lucide-svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import yaml from 'js-yaml'
|
||||
import hubPaths from '$lib/hubPaths.json'
|
||||
import { JobService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import FilterList from './FilterList.svelte'
|
||||
import { Tabs, Tab } from '$lib/components/common'
|
||||
|
||||
type ObjectType =
|
||||
| 'script'
|
||||
| 'flow'
|
||||
| 'app'
|
||||
| 'folder'
|
||||
| 'resource'
|
||||
| 'variable'
|
||||
| 'secret'
|
||||
| 'resourcetype'
|
||||
| 'schedule'
|
||||
| 'user'
|
||||
| 'group'
|
||||
| 'trigger'
|
||||
| 'settings'
|
||||
| 'key'
|
||||
|
||||
type GitSyncTypeMap = {
|
||||
scripts: boolean
|
||||
flows: boolean
|
||||
apps: boolean
|
||||
folders: boolean
|
||||
resourceTypes: boolean
|
||||
resources: boolean
|
||||
variables: boolean
|
||||
secrets: boolean
|
||||
schedules: boolean
|
||||
users: boolean
|
||||
groups: boolean
|
||||
triggers: boolean
|
||||
settings: boolean
|
||||
key: boolean
|
||||
}
|
||||
|
||||
type PreviewResult = {
|
||||
diff?: { [key: string]: { from: any; to: any } }
|
||||
hasChanges?: boolean
|
||||
isInitialSetup?: boolean
|
||||
message?: string
|
||||
local?: {
|
||||
include_path: string[]
|
||||
exclude_path: string[]
|
||||
extra_include_path: string[]
|
||||
include_type: ObjectType[]
|
||||
}
|
||||
backend?: {
|
||||
include_path: string[]
|
||||
exclude_path: string[]
|
||||
extra_include_path: string[]
|
||||
include_type: ObjectType[]
|
||||
}
|
||||
}
|
||||
|
||||
let {
|
||||
git_repo_resource_path = $bindable(''),
|
||||
include_path = $bindable(['f/**']),
|
||||
include_type = $bindable(['script', 'flow', 'app', 'folder'] as ObjectType[]),
|
||||
exclude_types_override = $bindable([] as ObjectType[]),
|
||||
isLegacyRepo = false,
|
||||
yamlText = $bindable(''),
|
||||
onSettingsChange = (settings: { yaml: string }) => {},
|
||||
excludes = $bindable([] as string[]),
|
||||
extraIncludes = $bindable([] as string[])
|
||||
} = $props()
|
||||
|
||||
// Component state
|
||||
let collapsed = $state(false)
|
||||
let editAsYaml = $state(false)
|
||||
let yamlError = $state('')
|
||||
let isPullMode = $state(false)
|
||||
|
||||
// Preview/Push state
|
||||
let previewResult = $state<PreviewResult | null>(null)
|
||||
let previewJobId = $state<string | null>(null)
|
||||
let previewJobStatus = $state<'running' | 'success' | 'failure' | undefined>(undefined)
|
||||
let pushJobId = $state<string | null>(null)
|
||||
let pushJobStatus = $state<'running' | 'success' | 'failure' | undefined>(undefined)
|
||||
let isPreviewLoading = $state(false)
|
||||
let isPushing = $state(false)
|
||||
let previewError = $state('')
|
||||
let previewSettingsSnapshot = $state<string | null>(null)
|
||||
|
||||
// Compute effective include types (include_type minus exclude_types_override for legacy repos only)
|
||||
const effectiveIncludeTypes = $derived(
|
||||
isLegacyRepo
|
||||
? include_type.filter((type) => !exclude_types_override.includes(type))
|
||||
: include_type
|
||||
)
|
||||
|
||||
// Compute type toggles from effective include types
|
||||
const typeToggles = $derived({
|
||||
scripts: effectiveIncludeTypes.includes('script'),
|
||||
flows: effectiveIncludeTypes.includes('flow'),
|
||||
apps: effectiveIncludeTypes.includes('app'),
|
||||
folders: effectiveIncludeTypes.includes('folder'),
|
||||
resourceTypes: effectiveIncludeTypes.includes('resourcetype'),
|
||||
resources: effectiveIncludeTypes.includes('resource'),
|
||||
variables: effectiveIncludeTypes.includes('variable'),
|
||||
secrets: effectiveIncludeTypes.includes('secret'),
|
||||
schedules: effectiveIncludeTypes.includes('schedule'),
|
||||
users: effectiveIncludeTypes.includes('user'),
|
||||
groups: effectiveIncludeTypes.includes('group'),
|
||||
triggers: effectiveIncludeTypes.includes('trigger'),
|
||||
settings: effectiveIncludeTypes.includes('settings'),
|
||||
key: effectiveIncludeTypes.includes('key')
|
||||
})
|
||||
|
||||
// Tab selection for filter kinds
|
||||
let filtersTab = $state<'includes' | 'excludes'>('includes')
|
||||
|
||||
function updateIncludeType(key: keyof GitSyncTypeMap, value: boolean) {
|
||||
const newTypes = new Set(include_type)
|
||||
const typeMap: Record<keyof GitSyncTypeMap, ObjectType> = {
|
||||
scripts: 'script',
|
||||
flows: 'flow',
|
||||
apps: 'app',
|
||||
folders: 'folder',
|
||||
resourceTypes: 'resourcetype',
|
||||
resources: 'resource',
|
||||
variables: 'variable',
|
||||
secrets: 'secret',
|
||||
schedules: 'schedule',
|
||||
users: 'user',
|
||||
groups: 'group',
|
||||
triggers: 'trigger',
|
||||
settings: 'settings',
|
||||
key: 'key'
|
||||
}
|
||||
|
||||
if (value) {
|
||||
newTypes.add(typeMap[key])
|
||||
} else {
|
||||
newTypes.delete(typeMap[key])
|
||||
if (key === 'variables') {
|
||||
newTypes.delete('secret')
|
||||
}
|
||||
}
|
||||
|
||||
include_type = Array.from(newTypes)
|
||||
}
|
||||
|
||||
function capitalize(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
|
||||
// Simple JSON-based UI state helper
|
||||
function getUIState() {
|
||||
return {
|
||||
include_path,
|
||||
exclude_path: excludes,
|
||||
extra_include_path: extraIncludes,
|
||||
include_type
|
||||
}
|
||||
}
|
||||
|
||||
// Apply settings from backend format (used by both local git repo and backend settings)
|
||||
function fromBackendFormat(settings: {
|
||||
include_path: string[]
|
||||
exclude_path: string[]
|
||||
extra_include_path: string[]
|
||||
include_type: ObjectType[]
|
||||
}) {
|
||||
include_path = settings.include_path || []
|
||||
excludes = settings.exclude_path || []
|
||||
extraIncludes = settings.extra_include_path || []
|
||||
include_type = settings.include_type || []
|
||||
}
|
||||
|
||||
// Simplified YAML parsing for manual editing
|
||||
function fromYaml(yamlStr: string) {
|
||||
yamlError = ''
|
||||
try {
|
||||
const parsed = yaml.load(yamlStr)
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
throw new Error('Invalid YAML structure')
|
||||
}
|
||||
|
||||
const obj: any = parsed
|
||||
yamlText = yamlStr
|
||||
|
||||
// Extract includes - reset to default if not present
|
||||
if (obj.includes && Array.isArray(obj.includes)) {
|
||||
include_path = obj.includes.map((p: any) => {
|
||||
if (typeof p !== 'string') {
|
||||
throw new Error('includes must contain only strings')
|
||||
}
|
||||
// Handle quoted strings
|
||||
if (/^['"].*['"]$/.test(p)) {
|
||||
return p.slice(1, -1).replace(/''/g, "'")
|
||||
}
|
||||
return p
|
||||
})
|
||||
} else {
|
||||
// Reset to default if includes is not present
|
||||
include_path = ['f/**']
|
||||
}
|
||||
|
||||
// Build the type set based on the YAML flags
|
||||
const newTypes = new Set<ObjectType>()
|
||||
|
||||
// Always include core types (these are fundamental and not controlled by flags)
|
||||
newTypes.add('script')
|
||||
newTypes.add('flow')
|
||||
newTypes.add('app')
|
||||
newTypes.add('folder')
|
||||
|
||||
// Handle skip flags (if skipX is false or undefined, include the type)
|
||||
if (obj.skipResourceTypes !== true) newTypes.add('resourcetype')
|
||||
if (obj.skipResources !== true) newTypes.add('resource')
|
||||
if (obj.skipVariables !== true) newTypes.add('variable')
|
||||
if (obj.skipSecrets !== true) newTypes.add('secret')
|
||||
|
||||
// Handle include flags (if includeX is true, include the type)
|
||||
if (obj.includeSchedules === true) newTypes.add('schedule')
|
||||
if (obj.includeTriggers === true) newTypes.add('trigger')
|
||||
if (obj.includeUsers === true) newTypes.add('user')
|
||||
if (obj.includeGroups === true) newTypes.add('group')
|
||||
if (obj.includeSettings === true) newTypes.add('settings')
|
||||
if (obj.includeKey === true) newTypes.add('key')
|
||||
|
||||
// Apply business rule: secrets can only be included if variables are included
|
||||
// This matches the UI behavior where turning off variables also turns off secrets
|
||||
if (!newTypes.has('variable')) {
|
||||
newTypes.delete('secret')
|
||||
}
|
||||
|
||||
include_type = Array.from(newTypes)
|
||||
} catch (e) {
|
||||
yamlError = e.message || 'Invalid YAML'
|
||||
console.error('Error parsing YAML:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// Simple YAML generation for manual editing mode
|
||||
function generateYamlFromUI() {
|
||||
try {
|
||||
const validIncludePath = include_path
|
||||
const validExcludePath = excludes
|
||||
const validExtraInclude = extraIncludes
|
||||
|
||||
// Basic YAML structure - let the CLI handle the proper normalization
|
||||
let config: any = {
|
||||
includes: validIncludePath,
|
||||
excludes: validExcludePath,
|
||||
extraIncludes: validExtraInclude,
|
||||
codebases: []
|
||||
}
|
||||
|
||||
// Let the CLI handle the optimization of skip/include flags
|
||||
// Just convert the UI state directly
|
||||
if (!include_type.includes('variable')) config.skipVariables = true
|
||||
if (!include_type.includes('resource')) config.skipResources = true
|
||||
if (!include_type.includes('secret')) config.skipSecrets = true
|
||||
if (!include_type.includes('resourcetype')) config.skipResourceTypes = true
|
||||
|
||||
if (include_type.includes('schedule')) config.includeSchedules = true
|
||||
if (include_type.includes('trigger')) config.includeTriggers = true
|
||||
if (include_type.includes('user')) config.includeUsers = true
|
||||
if (include_type.includes('group')) config.includeGroups = true
|
||||
if (include_type.includes('settings')) config.includeSettings = true
|
||||
if (include_type.includes('key')) config.includeKey = true
|
||||
|
||||
return yaml.dump(config, {
|
||||
indent: 2,
|
||||
lineWidth: -1,
|
||||
quotingType: '"',
|
||||
forceQuotes: false,
|
||||
noRefs: true
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn('Failed to generate YAML:', e)
|
||||
yamlError = e.message || 'Failed to generate YAML'
|
||||
return `includes:
|
||||
- f/**
|
||||
excludes: []
|
||||
extraIncludes: []
|
||||
codebases: []`
|
||||
}
|
||||
}
|
||||
|
||||
function switchToYaml() {
|
||||
yamlText = generateYamlFromUI()
|
||||
yamlError = ''
|
||||
editAsYaml = true
|
||||
}
|
||||
|
||||
function switchToUI() {
|
||||
fromYaml(yamlText)
|
||||
if (!yamlError) {
|
||||
editAsYaml = false
|
||||
}
|
||||
}
|
||||
|
||||
// Simplified preview function - always uses JSON approach
|
||||
async function previewFiltersToGitRepo() {
|
||||
isPreviewLoading = true
|
||||
previewError = ''
|
||||
previewResult = null
|
||||
previewJobId = null
|
||||
previewJobStatus = undefined
|
||||
|
||||
// Take a snapshot of current settings
|
||||
previewSettingsSnapshot = JSON.stringify({
|
||||
include_path,
|
||||
excludes,
|
||||
extraIncludes,
|
||||
include_type
|
||||
})
|
||||
|
||||
try {
|
||||
const workspace = $workspaceStore
|
||||
if (!workspace) return
|
||||
|
||||
// Always pass UI state as JSON - the backend now handles this uniformly
|
||||
const payloadObj = {
|
||||
workspace_id: workspace,
|
||||
repo_url_resource_path: git_repo_resource_path,
|
||||
only_wmill_yaml: true,
|
||||
dry_run: true,
|
||||
pull: isPullMode,
|
||||
settings_json: JSON.stringify(getUIState())
|
||||
}
|
||||
|
||||
const jobId = await JobService.runScriptByPath({
|
||||
workspace,
|
||||
path: hubPaths.gitInitRepo,
|
||||
requestBody: payloadObj,
|
||||
skipPreprocessor: true
|
||||
})
|
||||
|
||||
previewJobId = jobId
|
||||
previewJobStatus = 'running'
|
||||
let jobSuccess = false
|
||||
let result: PreviewResult = {}
|
||||
|
||||
await (
|
||||
await import('$lib/utils')
|
||||
).tryEvery({
|
||||
tryCode: async () => {
|
||||
const testResult = await JobService.getCompletedJob({ workspace, id: jobId })
|
||||
jobSuccess = !!testResult.success
|
||||
if (jobSuccess) {
|
||||
const jobResult = await JobService.getCompletedJobResult({ workspace, id: jobId })
|
||||
result = jobResult as PreviewResult
|
||||
}
|
||||
},
|
||||
timeoutCode: async () => {
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace,
|
||||
id: jobId,
|
||||
requestBody: { reason: 'Preview job timed out after 5s' }
|
||||
})
|
||||
} catch (err) {}
|
||||
},
|
||||
interval: 500,
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
previewJobStatus = jobSuccess ? 'success' : 'failure'
|
||||
if (jobSuccess) {
|
||||
previewResult = result
|
||||
} else {
|
||||
previewError = 'Preview failed'
|
||||
}
|
||||
} catch (e) {
|
||||
previewJobStatus = 'failure'
|
||||
previewError = e?.message || 'Preview failed'
|
||||
previewResult = null
|
||||
} finally {
|
||||
isPreviewLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
// Simplified push function - always uses JSON approach
|
||||
async function pushFiltersToGitRepo() {
|
||||
if (isPullMode) {
|
||||
// In pull mode, apply the local settings (from git repo) to UI
|
||||
if (previewResult?.local) {
|
||||
try {
|
||||
fromBackendFormat(previewResult.local)
|
||||
yamlText = generateYamlFromUI()
|
||||
onSettingsChange({ yaml: yamlText })
|
||||
sendUserToast('Changes applied - remember to save repository settings to persist changes')
|
||||
|
||||
// Clear the preview state after applying settings
|
||||
previewResult = null
|
||||
previewJobId = null
|
||||
previewJobStatus = undefined
|
||||
previewError = ''
|
||||
} catch (e) {
|
||||
previewError = 'Failed to apply pulled settings: ' + e.message
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Push mode - send current UI state as JSON
|
||||
isPushing = true
|
||||
pushJobId = null
|
||||
pushJobStatus = undefined
|
||||
|
||||
try {
|
||||
const workspace = $workspaceStore
|
||||
if (!workspace) return
|
||||
|
||||
const payloadObj = {
|
||||
workspace_id: workspace,
|
||||
repo_url_resource_path: git_repo_resource_path,
|
||||
dry_run: false,
|
||||
pull: isPullMode,
|
||||
only_wmill_yaml: true,
|
||||
settings_json: JSON.stringify(getUIState())
|
||||
}
|
||||
|
||||
const jobId = await JobService.runScriptByPath({
|
||||
workspace,
|
||||
path: hubPaths.gitInitRepo,
|
||||
requestBody: payloadObj,
|
||||
skipPreprocessor: true
|
||||
})
|
||||
|
||||
pushJobId = jobId
|
||||
pushJobStatus = 'running'
|
||||
let jobSuccess = false
|
||||
|
||||
await (
|
||||
await import('$lib/utils')
|
||||
).tryEvery({
|
||||
tryCode: async () => {
|
||||
const testResult = await JobService.getCompletedJob({ workspace, id: jobId })
|
||||
jobSuccess = !!testResult.success
|
||||
},
|
||||
timeoutCode: async () => {
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace,
|
||||
id: jobId,
|
||||
requestBody: { reason: 'Push job timed out after 5s' }
|
||||
})
|
||||
} catch (err) {}
|
||||
},
|
||||
interval: 500,
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
pushJobStatus = jobSuccess ? 'success' : 'failure'
|
||||
if (jobSuccess) {
|
||||
// Reset preview state after successful push
|
||||
previewResult = null
|
||||
previewJobId = null
|
||||
previewJobStatus = undefined
|
||||
previewError = ''
|
||||
}
|
||||
} catch (e) {
|
||||
pushJobStatus = 'failure'
|
||||
} finally {
|
||||
isPushing = false
|
||||
}
|
||||
}
|
||||
|
||||
// Simplified export function for backward compatibility
|
||||
export function toYaml() {
|
||||
return generateYamlFromUI()
|
||||
}
|
||||
|
||||
export function setSettings(settings: { yaml: string }) {
|
||||
yamlText = settings.yaml
|
||||
fromYaml(settings.yaml)
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
// Reset preview state when switching modes
|
||||
if (isPullMode !== undefined) {
|
||||
previewResult = null
|
||||
previewJobId = null
|
||||
previewJobStatus = undefined
|
||||
pushJobId = null
|
||||
pushJobStatus = undefined
|
||||
isPreviewLoading = false
|
||||
isPushing = false
|
||||
previewError = ''
|
||||
}
|
||||
})
|
||||
|
||||
// Reset preview state when settings change (making preview stale)
|
||||
$effect(() => {
|
||||
// Track all the settings that affect the preview
|
||||
const currentSettings = JSON.stringify({
|
||||
include_path,
|
||||
excludes,
|
||||
extraIncludes,
|
||||
include_type
|
||||
})
|
||||
|
||||
// If we have an existing preview result and settings have changed from snapshot, clear it
|
||||
if (
|
||||
previewResult !== null &&
|
||||
previewSettingsSnapshot !== null &&
|
||||
currentSettings !== previewSettingsSnapshot
|
||||
) {
|
||||
previewResult = null
|
||||
previewJobId = null
|
||||
previewJobStatus = undefined
|
||||
previewError = ''
|
||||
previewSettingsSnapshot = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="rounded-lg shadow-sm border p-0 w-full">
|
||||
<!-- Card Header -->
|
||||
<div class="flex items-center justify-between min-h-10 px-4 py-1 border-b">
|
||||
<div class="flex items-center gap-2">
|
||||
<Filter size={18} class="text-primary" />
|
||||
<span class="font-semibold text-sm">Git Sync filter settings</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if !collapsed}
|
||||
<button
|
||||
class="text-xs px-2 py-1 rounded border border-gray-300 bg-surface-primary hover:bg-surface-secondary"
|
||||
onclick={editAsYaml ? switchToUI : switchToYaml}
|
||||
>
|
||||
{editAsYaml ? 'Edit in UI' : 'Edit as YAML'}
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
class="text-gray-500 hover:text-primary focus:outline-none"
|
||||
onclick={() => (collapsed = !collapsed)}
|
||||
aria-label="Toggle collapse"
|
||||
>
|
||||
{#if collapsed}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 15l7-7 7 7"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if !collapsed}
|
||||
{#if editAsYaml}
|
||||
<div class="px-4 py-4">
|
||||
<textarea
|
||||
class="w-full h-64 font-mono text-xs border rounded p-2 bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
spellcheck="false"
|
||||
bind:value={yamlText}
|
||||
></textarea>
|
||||
{#if yamlError}
|
||||
<div class="text-xs text-red-600 mt-2">{yamlError}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="px-4 py-2">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 md:gap-32">
|
||||
<div class="flex flex-col gap-2">
|
||||
<Tabs bind:selected={filtersTab}>
|
||||
<Tab value="includes">Includes</Tab>
|
||||
<Tab value="excludes">Excludes</Tab>
|
||||
</Tabs>
|
||||
|
||||
{#if filtersTab === 'includes'}
|
||||
<FilterList
|
||||
title="Include path filters"
|
||||
bind:items={include_path}
|
||||
placeholder="Add filter (e.g. f/**)"
|
||||
>
|
||||
{#snippet tooltip()}
|
||||
<Tooltip>
|
||||
Only scripts, flows and apps with their path matching one of those filters will
|
||||
be synced to the Git repositories below. The filters allow '*' and '**'
|
||||
characters, with '*' matching any character allowed in paths until the next
|
||||
slash (/) and '**' matching anything including slashes. By default everything in
|
||||
folders will be synced.
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
</FilterList>
|
||||
{:else if filtersTab === 'excludes'}
|
||||
<FilterList
|
||||
title="Exclude path filters"
|
||||
bind:items={excludes}
|
||||
placeholder="Add filter (e.g. f/**)"
|
||||
>
|
||||
{#snippet tooltip()}
|
||||
<Tooltip>
|
||||
After the include / extra include checks, if a file matches any of these
|
||||
patterns it will be skipped.
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
</FilterList>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Type Filters Section (Right) -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<h4 class="font-semibold text-sm">Type filters</h4>
|
||||
<Tooltip>
|
||||
On top of the filter path above, you can include only certain type of object to be
|
||||
synced with the Git repository. By default everything is synced.
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Toggle
|
||||
size="xs"
|
||||
checked={typeToggles.scripts}
|
||||
on:change={(e) => updateIncludeType('scripts', e.detail)}
|
||||
options={{ right: capitalize('scripts') }}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Toggle
|
||||
size="xs"
|
||||
checked={typeToggles.flows}
|
||||
on:change={(e) => updateIncludeType('flows', e.detail)}
|
||||
options={{ right: capitalize('flows') }}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Toggle
|
||||
size="xs"
|
||||
checked={typeToggles.apps}
|
||||
on:change={(e) => updateIncludeType('apps', e.detail)}
|
||||
options={{ right: capitalize('apps') }}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Toggle
|
||||
size="xs"
|
||||
checked={typeToggles.folders}
|
||||
on:change={(e) => updateIncludeType('folders', e.detail)}
|
||||
options={{ right: capitalize('folders') }}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Toggle
|
||||
size="xs"
|
||||
checked={typeToggles.resourceTypes}
|
||||
on:change={(e) => updateIncludeType('resourceTypes', e.detail)}
|
||||
options={{ right: capitalize('resourceTypes') }}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Toggle
|
||||
size="xs"
|
||||
checked={typeToggles.resources}
|
||||
on:change={(e) => updateIncludeType('resources', e.detail)}
|
||||
options={{ right: capitalize('resources') }}
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-2 flex items-center gap-2">
|
||||
<Toggle
|
||||
size="xs"
|
||||
checked={typeToggles.variables}
|
||||
on:change={(e) => updateIncludeType('variables', e.detail)}
|
||||
options={{ right: 'Variables' }}
|
||||
/>
|
||||
<span class="text-gray-400">-</span>
|
||||
<Toggle
|
||||
size="xs"
|
||||
disabled={!typeToggles.variables}
|
||||
checked={typeToggles.secrets}
|
||||
on:change={(e) => updateIncludeType('secrets', e.detail)}
|
||||
options={{ left: 'Include secrets' }}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Toggle
|
||||
size="xs"
|
||||
checked={typeToggles.schedules}
|
||||
on:change={(e) => updateIncludeType('schedules', e.detail)}
|
||||
options={{ right: capitalize('schedules') }}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Toggle
|
||||
size="xs"
|
||||
checked={typeToggles.users}
|
||||
on:change={(e) => updateIncludeType('users', e.detail)}
|
||||
options={{ right: capitalize('users') }}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Toggle
|
||||
size="xs"
|
||||
checked={typeToggles.groups}
|
||||
on:change={(e) => updateIncludeType('groups', e.detail)}
|
||||
options={{ right: capitalize('groups') }}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Toggle
|
||||
size="xs"
|
||||
checked={typeToggles.triggers}
|
||||
on:change={(e) => updateIncludeType('triggers', e.detail)}
|
||||
options={{ right: capitalize('triggers') }}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Toggle
|
||||
size="xs"
|
||||
checked={typeToggles.settings}
|
||||
on:change={(e) => updateIncludeType('settings', e.detail)}
|
||||
options={{ right: 'Workspace settings' }}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Toggle
|
||||
size="xs"
|
||||
checked={typeToggles.key}
|
||||
on:change={(e) => updateIncludeType('key', e.detail)}
|
||||
options={{ right: 'Encryption key' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex flex-col gap-2 p-2">
|
||||
<div class="flex flex-col gap-2 mb-2">
|
||||
<Toggle
|
||||
size="sm"
|
||||
bind:checked={isPullMode}
|
||||
options={{
|
||||
left: 'Push',
|
||||
right: 'Pull'
|
||||
}}
|
||||
/>
|
||||
<span class="text-xs text-tertiary">
|
||||
{isPullMode ? 'Pull settings from Git repository' : 'Push settings to Git repository'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<Button
|
||||
size="sm"
|
||||
on:click={previewFiltersToGitRepo}
|
||||
disabled={isPreviewLoading || isPushing}
|
||||
startIcon={{
|
||||
icon: isPreviewLoading ? Loader2 : Eye,
|
||||
classes: isPreviewLoading ? 'animate-spin' : ''
|
||||
}}
|
||||
>
|
||||
{isPreviewLoading ? 'Previewing...' : 'Preview'}
|
||||
</Button>
|
||||
{#if previewResult?.hasChanges && (previewResult?.isInitialSetup || (previewResult?.diff && Object.keys(previewResult.diff).length > 0))}
|
||||
<Button
|
||||
size="sm"
|
||||
on:click={pushFiltersToGitRepo}
|
||||
disabled={isPushing || isPreviewLoading}
|
||||
color={isPullMode ? 'dark' : 'red'}
|
||||
startIcon={{
|
||||
icon: isPushing ? Loader2 : isPullMode ? Check : Save,
|
||||
classes: isPushing ? 'animate-spin' : ''
|
||||
}}
|
||||
>
|
||||
{isPushing
|
||||
? isPullMode
|
||||
? 'Applying...'
|
||||
: 'Pushing...'
|
||||
: isPullMode
|
||||
? 'Apply'
|
||||
: 'Push Settings to Git'}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if previewError}
|
||||
<div class="text-xs text-red-600 mt-2">{previewError}</div>
|
||||
{/if}
|
||||
{#if previewJobId}
|
||||
<div class="flex items-center gap-2 text-xs text-tertiary mt-1">
|
||||
{#if previewJobStatus === 'running'}
|
||||
<Loader2 class="animate-spin" size={14} />
|
||||
{:else if previewJobStatus === 'success'}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else if previewJobStatus === 'failure'}
|
||||
<XCircle size={14} class="text-red-700" />
|
||||
{/if}
|
||||
Preview job:
|
||||
<a
|
||||
target="_blank"
|
||||
class="underline"
|
||||
href={`/run/${previewJobId}?workspace=${$workspaceStore}`}>{previewJobId}</a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{#if previewResult}
|
||||
<div
|
||||
class="border rounded p-2 text-xs max-h-40 overflow-y-auto bg-surface-secondary mt-2"
|
||||
>
|
||||
<div class="font-semibold text-[11px] mb-1 text-tertiary">Preview of changes:</div>
|
||||
{#if previewResult.isInitialSetup}
|
||||
<div class="mt-2 text-green-600">
|
||||
{previewResult.message || 'wmill.yaml will be created with repository settings'}
|
||||
</div>
|
||||
{:else if previewResult.hasChanges && previewResult.diff && Object.keys(previewResult.diff).length > 0}
|
||||
<div class="mt-2 space-y-1">
|
||||
{#each Object.entries(previewResult.diff) as [field, change]}
|
||||
<div class="flex items-start gap-2 text-2xs">
|
||||
<span class="font-mono text-tertiary min-w-0 flex-shrink-0">{field}:</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
{#if Array.isArray(change.from) || Array.isArray(change.to)}
|
||||
<div class="space-y-0.5">
|
||||
<div class="text-red-600">- {JSON.stringify(change.from)}</div>
|
||||
<div class="text-green-600">+ {JSON.stringify(change.to)}</div>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-red-600">{JSON.stringify(change.from)}</span>
|
||||
<span class="text-tertiary"> → </span>
|
||||
<span class="text-green-600">{JSON.stringify(change.to)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-2 text-tertiary">No changes found! The file is up to date.</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if pushJobId}
|
||||
<div class="flex items-center gap-2 text-xs text-tertiary mt-1">
|
||||
{#if pushJobStatus === 'running'}
|
||||
<Loader2 class="animate-spin" size={14} />
|
||||
{:else if pushJobStatus === 'success'}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else if pushJobStatus === 'failure'}
|
||||
<XCircle size={14} class="text-red-700" />
|
||||
{/if}
|
||||
Push job:
|
||||
<a
|
||||
target="_blank"
|
||||
class="underline"
|
||||
href={`/run/${pushJobId}?workspace=${$workspaceStore}`}>{pushJobId}</a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -83,6 +83,7 @@ export function appToHubUrl(staticApp: any, hubBaseUrl: string): URL {
|
||||
type HubPaths = {
|
||||
gitSync: string
|
||||
gitSyncTest: string
|
||||
gitInitRepo: string
|
||||
slackErrorHandler: string
|
||||
slackRecoveryHandler: string
|
||||
slackSuccessHandler: string
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"gitSyncTest_1": "hub/11499/git-repo-test-read-write-windmill",
|
||||
"gitSyncTest_2": "hub/11667/git-repo-test-read-write-windmill",
|
||||
"gitSyncTest": "hub/11669/git-repo-test-read-write-windmill",
|
||||
"gitInitRepo": "hub/19740/git-sync%3A-init-repository-windmill",
|
||||
"slackErrorHandler": "hub/19741/workspace-or-schedule-error-handler-slack",
|
||||
"slackErrorHandler_0": "hub/9079/workspace-or-schedule-error-handler-slack",
|
||||
"slackErrorHandler_1": "hub/9206/workspace-or-schedule-error-handler-slack",
|
||||
|
||||
@@ -156,8 +156,8 @@
|
||||
<div class="h-screen">
|
||||
{#key value}
|
||||
<AppEditor
|
||||
on:savedNewAppPath={(event) => {
|
||||
goto(`/apps/edit/${event.detail}`)
|
||||
onSavedNewAppPath={(path) => {
|
||||
goto(`/apps/edit/${path}`)
|
||||
}}
|
||||
{summary}
|
||||
app={value}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -4,8 +4,8 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
wmill = ">=1.505.3"
|
||||
wmill_pg = ">=1.505.3"
|
||||
wmill = ">=1.507.0"
|
||||
wmill_pg = ">=1.507.0"
|
||||
sendgrid = "*"
|
||||
mysql-connector-python = "*"
|
||||
pymongo = "*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.505.3
|
||||
version: 1.507.0
|
||||
title: OpenFlow Spec
|
||||
contact:
|
||||
name: Ruben Fiszel
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
RootModule = 'WindmillClient.psm1'
|
||||
|
||||
# Version number of this module.
|
||||
ModuleVersion = '1.505.3'
|
||||
ModuleVersion = '1.507.0'
|
||||
|
||||
# Supported PSEditions
|
||||
# CompatiblePSEditions = @()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
description = "A client library for accessing Windmill server wrapping the Windmill client API"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill-pg"
|
||||
version = "1.505.3"
|
||||
version = "1.507.0"
|
||||
description = "An extension client for the wmill client library focused on pg"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill/windmill",
|
||||
"version": "1.505.3",
|
||||
"version": "1.507.0",
|
||||
"exports": "./src/index.ts",
|
||||
"publish": {
|
||||
"exclude": ["!src", "./s3Types.ts", "./client.ts"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "windmill-client",
|
||||
"description": "Windmill SDK client for browsers and Node.js",
|
||||
"version": "1.505.3",
|
||||
"version": "1.507.0",
|
||||
"author": "Ruben Fiszel",
|
||||
"license": "Apache 2.0",
|
||||
"devDependencies": {
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.505.3
|
||||
1.507.0
|
||||
|
||||
Reference in New Issue
Block a user