mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 00:02:19 +00:00
4342c18541
* feat: add CLI workspace merge command and enhance fork with datatable/color support Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: abort fork on git branch failure, per-datatable error handling, guard resetDiffTally Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test: add fork/merge integration tests covering full cycle Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: support deploying deletions during fork merge (archive/delete in target) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: share deploy logic between CLI and frontend via windmill-utils-internal Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: revert frontend to self-contained deploy, fix failure_module handling The frontend imports windmill-utils-internal from npm (published v1.3.4) which doesn't have the new deploy module yet. Revert frontend to its own self-contained implementation with two improvements: - Pass failure_module to getAllModules in flow deploy and getItemValue - Add deleteItemInWorkspace for deploying deletions during merge The shared deploy.ts in windmill-utils-internal remains for CLI use. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: share deploy logic via published windmill-utils-internal, add comprehensive integration tests - Publish windmill-utils-internal v1.3.8 with DeployProvider interface - Frontend now uses shared deploy module (deployItem, deleteItemInWorkspace, checkItemExists, getOnBehalfOf, getItemValue) via provider adapter - Add 4 new integration test sub-tests: all item types, secret variables, special characters, partial deploy + resetDiffTally Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: remove unused folderName function from frontend utils_workspace_deploy Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
238 lines
7.5 KiB
TypeScript
238 lines
7.5 KiB
TypeScript
import {
|
|
AppService,
|
|
FlowService,
|
|
FolderService,
|
|
ResourceService,
|
|
ScheduleService,
|
|
ScriptService,
|
|
VariableService
|
|
} from '$lib/gen'
|
|
import {
|
|
existsTrigger,
|
|
getTriggersDeployData,
|
|
getTriggerPermissionedAs,
|
|
getTriggerValue,
|
|
type AdditionalInformation,
|
|
type Kind
|
|
} from '$lib/utils_deployable'
|
|
import type { TriggerKind } from './components/triggers'
|
|
|
|
import {
|
|
deployItem as sharedDeployItem,
|
|
deleteItemInWorkspace as sharedDeleteItem,
|
|
checkItemExists as sharedCheckItemExists,
|
|
getOnBehalfOf as sharedGetOnBehalfOf,
|
|
getItemValue as sharedGetItemValue,
|
|
type DeployProvider,
|
|
type DeployKind,
|
|
type DeployResult
|
|
} from 'windmill-utils-internal'
|
|
|
|
export type { DeployResult }
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Provider adapter — wraps frontend's class-based services
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function makeProvider(): DeployProvider {
|
|
return {
|
|
existsFlowByPath: (p) => FlowService.existsFlowByPath(p),
|
|
existsScriptByPath: (p) => ScriptService.existsScriptByPath(p),
|
|
existsApp: (p) => AppService.existsApp(p),
|
|
existsVariable: (p) => VariableService.existsVariable(p),
|
|
existsResource: (p) => ResourceService.existsResource(p),
|
|
existsResourceType: (p) => ResourceService.existsResourceType(p),
|
|
existsFolder: (p) => FolderService.existsFolder(p),
|
|
getFlowByPath: (p) => FlowService.getFlowByPath(p),
|
|
createFlow: (p) => FlowService.createFlow(p),
|
|
updateFlow: (p) => FlowService.updateFlow(p),
|
|
archiveFlowByPath: (p) => FlowService.archiveFlowByPath(p),
|
|
getScriptByPath: (p) => ScriptService.getScriptByPath(p),
|
|
createScript: (p) => ScriptService.createScript(p),
|
|
archiveScriptByPath: (p) => ScriptService.archiveScriptByPath(p),
|
|
getAppByPath: (p) => AppService.getAppByPath(p),
|
|
createApp: (p) => AppService.createApp(p),
|
|
updateApp: (p) => AppService.updateApp(p),
|
|
createAppRaw: (p) => AppService.createAppRaw(p),
|
|
updateAppRaw: (p) => AppService.updateAppRaw(p),
|
|
getPublicSecretOfLatestVersionOfApp: (p) => AppService.getPublicSecretOfLatestVersionOfApp(p),
|
|
getRawAppData: (p) => AppService.getRawAppData(p),
|
|
deleteApp: (p) => AppService.deleteApp(p),
|
|
getVariable: (p) => VariableService.getVariable(p),
|
|
createVariable: (p) => VariableService.createVariable(p),
|
|
updateVariable: (p) => VariableService.updateVariable(p),
|
|
deleteVariable: (p) => VariableService.deleteVariable(p),
|
|
getResource: (p) => ResourceService.getResource(p),
|
|
createResource: (p) => ResourceService.createResource(p),
|
|
updateResource: (p) => ResourceService.updateResource(p),
|
|
deleteResource: (p) => ResourceService.deleteResource(p),
|
|
getResourceType: (p) => ResourceService.getResourceType(p),
|
|
createResourceType: (p) => ResourceService.createResourceType(p),
|
|
updateResourceType: (p) => ResourceService.updateResourceType(p),
|
|
deleteResourceType: (p) => ResourceService.deleteResourceType(p),
|
|
getFolder: (p) => FolderService.getFolder(p),
|
|
createFolder: (p) => FolderService.createFolder(p),
|
|
updateFolder: (p) => FolderService.updateFolder(p),
|
|
deleteFolder: (p) => FolderService.deleteFolder(p)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public API — thin wrappers that add trigger handling (frontend-specific)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface DeployItemParams {
|
|
kind: Kind
|
|
path: string
|
|
workspaceFrom: string
|
|
workspaceTo: string
|
|
additionalInformation?: AdditionalInformation
|
|
/**
|
|
* The value to use for on_behalf_of when deploying.
|
|
* Format varies by item kind:
|
|
* - For flows/scripts/apps: an email address (on_behalf_of_email)
|
|
* - For triggers/schedules: permissioned_as format (u/username or g/group)
|
|
* If set, preserve_on_behalf_of / preserve_permissioned_as will be true.
|
|
* If undefined, the deploying user's identity is used.
|
|
*/
|
|
onBehalfOf?: string
|
|
}
|
|
|
|
/**
|
|
* Deploy an item from one workspace to another.
|
|
* Handles all item kinds: flow, script, app, variable, resource, resource_type, folder, trigger.
|
|
*/
|
|
export async function deployItem(params: DeployItemParams): Promise<DeployResult> {
|
|
const { kind, path, workspaceFrom, workspaceTo, additionalInformation, onBehalfOf } = params
|
|
|
|
// Triggers are frontend-specific (not in the shared module)
|
|
if (kind === 'trigger') {
|
|
if (!additionalInformation?.triggers) {
|
|
return { success: false, error: 'Missing triggers kind' }
|
|
}
|
|
try {
|
|
const alreadyExists = await checkItemExists(kind, path, workspaceTo, additionalInformation)
|
|
const { data, createFn, updateFn } = await getTriggersDeployData(
|
|
additionalInformation.triggers.kind,
|
|
path,
|
|
workspaceFrom,
|
|
onBehalfOf
|
|
)
|
|
if (alreadyExists) {
|
|
await updateFn({ path, workspace: workspaceTo, requestBody: data } as any)
|
|
} else {
|
|
await createFn({ workspace: workspaceTo, requestBody: data } as any)
|
|
}
|
|
return { success: true }
|
|
} catch (e: any) {
|
|
return { success: false, error: e.body || e.message || String(e) }
|
|
}
|
|
}
|
|
|
|
return sharedDeployItem(
|
|
makeProvider(),
|
|
kind as DeployKind,
|
|
path,
|
|
workspaceFrom,
|
|
workspaceTo,
|
|
onBehalfOf
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Delete/archive an item in a workspace.
|
|
* Used when deploying a deletion from one workspace to another.
|
|
* Scripts and flows are archived (reversible). Other types are deleted.
|
|
*/
|
|
export async function deleteItemInWorkspace(
|
|
kind: Kind,
|
|
path: string,
|
|
workspace: string
|
|
): Promise<DeployResult> {
|
|
return sharedDeleteItem(makeProvider(), kind as DeployKind, path, workspace)
|
|
}
|
|
|
|
/**
|
|
* Check if an item already exists in the target workspace.
|
|
*/
|
|
export async function checkItemExists(
|
|
kind: Kind,
|
|
path: string,
|
|
workspace: string,
|
|
additionalInformation?: AdditionalInformation
|
|
): Promise<boolean> {
|
|
// Triggers and schedules are frontend-specific
|
|
if (kind === 'schedule') {
|
|
return ScheduleService.existsSchedule({ workspace, path })
|
|
} else if (kind === 'trigger') {
|
|
const triggersKind: TriggerKind[] = [
|
|
'kafka',
|
|
'mqtt',
|
|
'nats',
|
|
'postgres',
|
|
'routes',
|
|
'schedules',
|
|
'sqs',
|
|
'websockets',
|
|
'gcp'
|
|
]
|
|
if (
|
|
additionalInformation?.triggers &&
|
|
triggersKind.includes(additionalInformation.triggers.kind)
|
|
) {
|
|
return existsTrigger({ workspace, path }, additionalInformation.triggers.kind)
|
|
} else {
|
|
throw new Error(
|
|
`Unexpected triggers kind, expected one of: '${triggersKind.join(', ')}' got: ${additionalInformation?.triggers?.kind}`
|
|
)
|
|
}
|
|
}
|
|
|
|
return sharedCheckItemExists(makeProvider(), kind as DeployKind, path, workspace)
|
|
}
|
|
|
|
/**
|
|
* Get the value of an item for diff comparison.
|
|
*/
|
|
export async function getItemValue(
|
|
kind: Kind,
|
|
path: string,
|
|
workspace: string,
|
|
additionalInformation?: AdditionalInformation
|
|
): Promise<unknown> {
|
|
// Triggers are frontend-specific
|
|
if (kind === 'trigger') {
|
|
if (additionalInformation?.triggers) {
|
|
try {
|
|
return await getTriggerValue(additionalInformation.triggers.kind, path, workspace)
|
|
} catch {
|
|
return {}
|
|
}
|
|
}
|
|
return {}
|
|
}
|
|
|
|
return sharedGetItemValue(makeProvider(), kind as DeployKind, path, workspace)
|
|
}
|
|
|
|
/**
|
|
* Get the on_behalf_of value for a deployable item.
|
|
*/
|
|
export async function getOnBehalfOf(
|
|
kind: Kind,
|
|
path: string,
|
|
workspace: string,
|
|
additionalInformation?: AdditionalInformation
|
|
): Promise<string | undefined> {
|
|
// Triggers are frontend-specific
|
|
if (kind === 'trigger' && additionalInformation?.triggers) {
|
|
try {
|
|
return await getTriggerPermissionedAs(additionalInformation.triggers.kind, path, workspace)
|
|
} catch {
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
return sharedGetOnBehalfOf(makeProvider(), kind as DeployKind, path, workspace)
|
|
}
|