feat(cli): non-interactive Slack connect/disconnect + sync round-trip fixes (#8935)

* feat(cli): non-interactive Slack connect/disconnect

Extract create_slack_workspace_artifacts / create_slack_instance_artifacts
from the browser OAuth callbacks and expose them via two new endpoints that
accept a pre-minted xoxb bot token:

- POST /w/{workspace}/workspaces/connect_slack (admin)
- POST /oauth/connect_slack_instance (super-admin)

Both produce bit-for-bit identical DB state to the UI browser flow.

Wire three CLI commands as thin wrappers:
- wmill workspace connect-slack
- wmill workspace disconnect-slack
- wmill instance connect-slack

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): round-trip stability for workspace settings handlers

wmill sync push was destroying UI-configured error_handler/success_handler
state on every deploy. Two orthogonal bugs:

(a) pushWorkspaceSettings called editErrorHandler with `path: undefined`
    when the YAML lacked the handler block, which the backend treats as a
    clear — so syncing settings.yaml that didn't mention the handler wiped
    the DB row. Fix: skip the call entirely when absent from YAML.

(b) edit_error_handler omitted muted_on_cancel / muted_on_user_path when
    false, but the CLI always sends them, causing perpetual deepEqual
    drift and a spurious editErrorHandler call on every sync push. Fix:
    always persist both booleans.

migrateToGroupedFormat now preserves explicit `null` on
error_handler / success_handler as a "clear remote" signal distinct from
absence. Widen ErrorHandlerConfig | null / SuccessHandlerConfig | null to
make this explicit in the type.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli): sync support for workspace-level Slack OAuth override

Add slack_oauth_client_id and slack_oauth_client_secret to the v2 tarball
export and to pushWorkspaceSettings, so the workspace-level OAuth override
is now fully managed as code through settings.yaml.

Semantics:
  - both defined and truthy → setWorkspaceSlackOauthConfig (upsert)
  - both defined but falsy (e.g. empty strings) and remote has a value
    → deleteWorkspaceSlackOauthConfig
  - either omitted → leave remote alone ("not managed by git")

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(cli): normalize workspace settings sync to "omit = clear"

Earlier commits on this branch introduced an "omit = keep" rule for
error_handler / success_handler / slack_oauth_client_{id,secret} that
diverged from every other workspace setting (webhook, deploy_to, etc. all
treat YAML as canonical: absence = clear). Normalize:

- v2 tarball always emits these 4 fields (null when remote is NULL) so
  round-trip is bijective and settings.yaml is a complete snapshot.
- pushWorkspaceSettings drops the absent-from-YAML guards; YAML is
  canonical. Absence and explicit null both clear the remote — same rule
  as every other field.
- set_slack_oauth_config / delete_slack_oauth_config now fire
  handle_deployment_metadata so UI mutations reach git-sync-enabled
  workspaces' committed settings.yaml.

Policy for users: pull before push (same as every other setting). On first
post-upgrade pull, explicit `null` keys appear for any workspace whose
handlers / oauth override are unset — one-time YAML diff, no semantic
change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(cli): add unit + integration coverage for Slack settings sync

Unit tests (settings_unit.test.ts): cover migrateToGroupedFormat preserving
explicit `null` on error_handler / success_handler, and passthrough of
slack_oauth_client_id / _secret (both populated and null values).

Integration tests (slack_settings_sync.test.ts, skipped on CI per the same
convention as datatable_settings_sync.test.ts): exercise the full backend
via withTestBackend to verify

  1. pull emits null for unset error_handler / success_handler /
     slack_oauth_client_id / _secret;
  2. round-trip with all-null handlers is idempotent;
  3. push of populated slack_oauth_config upserts;
  4. omitting the slack_oauth keys from YAML clears remote (universal
     "omit = clear" rule);
  5. explicit null error_handler in YAML clears remote;
  6. round-trip preserves a populated error_handler exactly, including the
     always-persisted muted_on_cancel / muted_on_user_path booleans.

Also feature-gates `use crate::oauth2_oss::workspace_connect_slack` and its
route registration behind `cfg(feature = "oauth2")`: the import caused a
build failure on subsets of the workspace without the oauth2 feature,
surfaced by the integration test harness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: bump ee-repo-ref to 59b6123

Pins windmill-ee-private to the tip of branch alp/slack_cli, which
contains the companion EE changes (helper extraction, non-interactive
Slack connect handlers, git-sync for Slack settings mutations).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Update SQLx metadata

* chore: regenerate system prompts for new slack CLI commands

Captures the new workspace connect-slack, workspace disconnect-slack,
and instance connect-slack commands in the auto-generated files that
CI enforces via system_prompts/check-freshness.sh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to b4a5ca11e3b96ff03793c2bd396dbc1fe6ea1022

This commit updates the EE repository reference after PR #550 was merged in windmill-ee-private.

Previous ee-repo-ref: d7e44d0519327ec9077625130365e887826f324b

New ee-repo-ref: b4a5ca11e3b96ff03793c2bd396dbc1fe6ea1022

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
Alexander Petric
2026-04-24 17:14:08 +00:00
committed by GitHub
co-authored by Claude Opus 4.7 windmill-internal-app[bot] Ruben Fiszel
parent a1a73309fd
commit 95d4c6a94d
20 changed files with 874 additions and 45 deletions
+47
View File
@@ -0,0 +1,47 @@
import { GlobalOptions } from "../../types.ts";
import { colors } from "@cliffy/ansi/colors";
import * as log from "../../core/log.ts";
import { requireLogin } from "../../core/auth.ts";
import { resolveWorkspace } from "../../core/context.ts";
import * as wmill from "../../../gen/services.gen.ts";
export async function connectSlack(
opts: GlobalOptions & {
botToken: string;
teamId: string;
teamName: string;
}
) {
await requireLogin(opts);
const workspace = await resolveWorkspace(opts);
await wmill.connectSlack({
workspace: workspace.workspaceId,
requestBody: {
bot_token: opts.botToken,
team_id: opts.teamId,
team_name: opts.teamName,
},
});
log.info(
colors.bold.underline.green(
`Slack connected to workspace ${workspace.workspaceId} (team ${opts.teamName} / ${opts.teamId})`
)
);
}
export async function disconnectSlack(opts: GlobalOptions) {
await requireLogin(opts);
const workspace = await resolveWorkspace(opts);
await wmill.disconnectSlack({ workspace: workspace.workspaceId });
log.info(
colors.bold.underline.green(
`Slack disconnected from workspace ${workspace.workspaceId} (slack_team_id / slack_name cleared). ` +
`To also remove the bot token variable/resource/folder/group, delete the corresponding files from the local sync folder and run 'wmill sync push'. ` +
`To remove the workspace-level OAuth override (if any), set slack_oauth_client_id/_secret to '' in settings.yaml and push.`
)
);
}
+15 -1
View File
@@ -17,6 +17,7 @@ import { setClient } from "../../core/client.ts";
import { requireLogin } from "../../core/auth.ts";
import { createWorkspaceFork, deleteWorkspaceFork } from "./fork.ts";
import { mergeWorkspaces } from "./merge.ts";
import { connectSlack, disconnectSlack } from "./slack.ts";
import * as wmill from "../../../gen/services.gen.ts";
@@ -787,6 +788,19 @@ const command = new Command()
.option("--exclude <items:string>", "Comma-separated kind:path items to exclude")
.option("--preserve-on-behalf-of", "Preserve original on_behalf_of/permissioned_as values")
.option("-y --yes", "Non-interactive mode (deploy without prompts)")
.action(mergeWorkspaces as any);
.action(mergeWorkspaces as any)
.command("connect-slack")
.description(
"Non-interactively connect Slack to the active workspace using a pre-minted bot token (xoxb-...). Produces the same artifacts as the UI OAuth flow: workspace_settings fields, g/slack group, f/slack_bot folder, and the encrypted bot token variable + resource at f/slack_bot/bot_token."
)
.option("--bot-token <bot_token:string>", "Slack bot token (xoxb-...)", { required: true })
.option("--team-id <team_id:string>", "Slack team id", { required: true })
.option("--team-name <team_name:string>", "Slack team name", { required: true })
.action(connectSlack as any)
.command("disconnect-slack")
.description(
"Clear slack_team_id / slack_name on the active workspace (marks the workspace as disconnected). Does NOT remove the bot token variable/resource/folder/group — delete those from the local sync folder and run 'wmill sync push' to tear them down. Does NOT remove the workspace-level OAuth override — set slack_oauth_client_id/_secret to '' in settings.yaml and push."
)
.action(disconnectSlack as any);
export default command;