mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
feat: git sync improvements (#6182)
* init checkpoint * ui second pass... * round 1 backend + saving settings + detecting changes... * checkpoint * fix openapi * saving + correct wmill.yaml diff * cli refactor * cli and tests refactor done * cli multi workspace support * cli support skip core types to align with ui * new test framework * sqlx * openapi spec * frontend * sync + settings changes * some fixes * some fixes * security: Remove hardcoded EE license key, use environment variable only - Remove hardcoded license key from containerized test backend - Environment variable EE_LICENSE_KEY now required for EE features - License key no longer stored in database during tests * sqlx * tests * fixing tests * fix tests * checkpoint * checkpoint * cli build * frontend - cli exchange * settings match * ee repo ref * npm check * openapi * tests * checkpoint * cli + tests * reset to preview on changes * merge issue ee * cleanup * hubscript * simplifications * ee repo ref * cli fixes * fix sync and add tests * extra test * git sync settings / key change aware * ee-repo ref * ee-repo ref * ee repo ref * ee ref * review 1 * ee ref * Update frontend/src/lib/components/PullGitRepoPopover.svelte Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * Update frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * ee ref * remove extra includes from ui --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
+2
-2
@@ -18,8 +18,8 @@
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76"
|
||||
|
||||
+5
-5
@@ -41,11 +41,11 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1 +1 @@
|
||||
22aa4778f97d034e5faea8b8d8c971c89ce74137
|
||||
0e1db1beeac098e8f2ddd2a0abfe9f83751b2092
|
||||
@@ -581,7 +581,7 @@ paths:
|
||||
summary: get connected repositories
|
||||
operationId: getGlobalConnectedRepositories
|
||||
tags:
|
||||
- git_sync
|
||||
- Git Sync
|
||||
responses:
|
||||
"200":
|
||||
description: connected repositories
|
||||
@@ -12304,8 +12304,7 @@ paths:
|
||||
"200":
|
||||
description: Parquet Preview
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
application/json: {}
|
||||
|
||||
/w/{workspace}/job_helpers/load_table_count/{path}:
|
||||
get:
|
||||
@@ -12384,8 +12383,7 @@ paths:
|
||||
"200":
|
||||
description: Csv Preview
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
application/json: {}
|
||||
|
||||
/w/{workspace}/job_helpers/delete_s3_file:
|
||||
delete:
|
||||
@@ -17006,27 +17004,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:
|
||||
@@ -17078,6 +17055,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:
|
||||
@@ -17095,6 +17108,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
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,
|
||||
|
||||
+347
-194
@@ -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) => {
|
||||
@@ -66,198 +68,349 @@ export {
|
||||
export const VERSION = "1.506.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))
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user