From 39e77ecd002b41630fa8d146ee0f15369656acda Mon Sep 17 00:00:00 2001 From: Alexander Petric Date: Mon, 9 Mar 2026 17:39:03 -0400 Subject: [PATCH] feat: add slack connection fields to workspace settings export/import (#8287) Co-authored-by: Claude Opus 4.6 --- backend/windmill-api/src/workspaces_export.rs | 26 +++++++++++++++++- cli/src/core/settings.ts | 27 ++++++++++++++++++- cli/test/settings_unit.test.ts | 16 +++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/backend/windmill-api/src/workspaces_export.rs b/backend/windmill-api/src/workspaces_export.rs index 091c465a61..cd32587648 100644 --- a/backend/windmill-api/src/workspaces_export.rs +++ b/backend/windmill-api/src/workspaces_export.rs @@ -282,6 +282,12 @@ struct SimplifiedSettings { color: Option, #[serde(skip_serializing_if = "Option::is_none")] operator_settings: Option, + #[serde(skip_serializing_if = "Option::is_none")] + slack_team_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + slack_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + slack_command_script: Option, } // V1 format: Legacy flat format for backward compatibility (matches main branch exactly) @@ -316,6 +322,12 @@ struct SimplifiedSettingsLegacy { color: Option, #[serde(skip_serializing_if = "Option::is_none")] operator_settings: Option, + #[serde(skip_serializing_if = "Option::is_none")] + slack_team_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + slack_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + slack_command_script: Option, } // Internal struct for querying database @@ -335,6 +347,9 @@ struct SettingsRow { mute_critical_alerts: Option, color: Option, operator_settings: Option, + slack_team_id: Option, + slack_name: Option, + slack_command_script: Option, } pub(crate) async fn tarball_workspace( @@ -939,7 +954,10 @@ pub(crate) async fn tarball_workspace( workspace.name as name, mute_critical_alerts, color, - operator_settings + operator_settings, + slack_team_id, + slack_name, + slack_command_script FROM workspace_settings LEFT JOIN workspace ON workspace.id = workspace_settings.workspace_id WHERE workspace_id = $1"#, @@ -965,6 +983,9 @@ pub(crate) async fn tarball_workspace( mute_critical_alerts: row.mute_critical_alerts, color: row.color.clone(), operator_settings: row.operator_settings.clone(), + slack_team_id: row.slack_team_id.clone(), + slack_name: row.slack_name.clone(), + slack_command_script: row.slack_command_script.clone(), }; serde_json::to_value(settings) .map(|v| serde_json::to_string_pretty(&v).ok()) @@ -1024,6 +1045,9 @@ pub(crate) async fn tarball_workspace( mute_critical_alerts: row.mute_critical_alerts, color: row.color, operator_settings: row.operator_settings, + slack_team_id: row.slack_team_id, + slack_name: row.slack_name, + slack_command_script: row.slack_command_script, }; serde_json::to_value(settings) .map(|v| serde_json::to_string_pretty(&v).ok()) diff --git a/cli/src/core/settings.ts b/cli/src/core/settings.ts index 3ffddc4837..0f7acce4a9 100644 --- a/cli/src/core/settings.ts +++ b/cli/src/core/settings.ts @@ -53,6 +53,9 @@ export interface SimplifiedSettings { mute_critical_alerts?: boolean; color?: string; operator_settings?: any; + slack_team_id?: string; + slack_name?: string; + slack_command_script?: string; } // Legacy settings interface for reading old settings.yaml files @@ -77,6 +80,9 @@ interface LegacySimplifiedSettings { mute_critical_alerts?: boolean; color?: string; operator_settings?: any; + slack_team_id?: string; + slack_name?: string; + slack_command_script?: string; } // Helper to convert legacy flat settings to new grouped format @@ -94,6 +100,9 @@ export function migrateToGroupedFormat(settings: any): SimplifiedSettings { if (settings.mute_critical_alerts !== undefined) result.mute_critical_alerts = settings.mute_critical_alerts; if (settings.color !== undefined) result.color = settings.color; if (settings.operator_settings !== undefined) result.operator_settings = settings.operator_settings; + if (settings.slack_team_id !== undefined) result.slack_team_id = settings.slack_team_id; + if (settings.slack_name !== undefined) result.slack_name = settings.slack_name; + if (settings.slack_command_script !== undefined) result.slack_command_script = settings.slack_command_script; // Handle auto_invite: check if already grouped or needs migration if (settings.auto_invite && typeof settings.auto_invite === "object") { @@ -183,12 +192,18 @@ export async function pushWorkspaceSettings( mute_critical_alerts: remoteSettings.mute_critical_alerts, color: remoteSettings.color, operator_settings: remoteSettings.operator_settings, + slack_team_id: remoteSettings.slack_team_id, + slack_name: remoteSettings.slack_name, + slack_command_script: remoteSettings.slack_command_script, }; } catch (err) { throw new Error(`Failed to get workspace settings: ${err}`); } - if (isSuperset(localSettings, settings)) { + // Exclude read-only fields from comparison (slack_team_id and slack_name are set via OAuth only) + const { slack_team_id: _lst, slack_name: _lsn, ...comparableLocal } = localSettings; + const { slack_team_id: _rst, slack_name: _rsn, ...comparableRemote } = settings; + if (isSuperset(comparableLocal, comparableRemote)) { log.debug(`Workspace settings are up to date`); return; } @@ -366,6 +381,16 @@ export async function pushWorkspaceSettings( requestBody: localSettings.operator_settings, }); } + + if (localSettings.slack_command_script != settings.slack_command_script) { + log.debug(`Updating slack command script...`); + await wmill.editSlackCommand({ + workspace, + requestBody: { + slack_command_script: localSettings.slack_command_script, + }, + }); + } } export async function pushWorkspaceKey( diff --git a/cli/test/settings_unit.test.ts b/cli/test/settings_unit.test.ts index 2d6a5249ef..014456cfc0 100644 --- a/cli/test/settings_unit.test.ts +++ b/cli/test/settings_unit.test.ts @@ -193,5 +193,21 @@ describe("migrateToGroupedFormat", () => { expect("webhook" in result).toBe(false); expect("deploy_to" in result).toBe(false); expect("color" in result).toBe(false); + expect("slack_team_id" in result).toBe(false); + expect("slack_name" in result).toBe(false); + expect("slack_command_script" in result).toBe(false); + }); + + test("copies slack fields through", () => { + const settings = { + name: "ws", + slack_team_id: "T12345", + slack_name: "my-team", + slack_command_script: "u/admin/slack_handler", + }; + const result = migrateToGroupedFormat(settings); + expect(result.slack_team_id).toBe("T12345"); + expect(result.slack_name).toBe("my-team"); + expect(result.slack_command_script).toBe("u/admin/slack_handler"); }); });