Files
windmill/frontend/src/lib/rawAppDeploy.ts
T
Ruben Fiszel f99781ca5f fix: persist on-behalf-of user across app deploy paths (#9773)
* fix(frontend): persist on-behalf-of user when redeploying raw apps

The raw-app deploy drawer reused AppEditorHeaderDeploy but never wired up
the `preserveOnBehalfOf` bindable nor forwarded `preserve_on_behalf_of` in
the createAppRaw/updateAppRaw request bodies. Without that flag, the shared
backend handler (create_app_internal/update_app_internal) resets the policy's
on_behalf_of to the deploying user on every deploy. So a publisher who set
"App executed on behalf of <other user>" would silently lose it on the next
deploy, unlike every other setting on the deploy page.

Mirror the classic (low-code) app header: declare `preserveOnBehalfOf`, bind
it to the deploy component, and send `preserve_on_behalf_of` on both create
and update.

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

* fix(frontend): preserve on-behalf-of in the draft-deploy path

The draft-deploy path (deployDraft → AppService.createApp/updateApp for visual
apps, deployRawAppDraft → createAppRaw/updateAppRaw for raw apps) carries the
deployed app's policy forward but never sent preserve_on_behalf_of. So
deploying a draft via the "Review & deploy drafts" UI silently reset the
policy's on_behalf_of to the deploying user — the same backend reset behind the
deploy-drawer bug, on a surface that has no on-behalf-of selector to re-set it.

Send preserve_on_behalf_of whenever the carried policy has an on_behalf_of, for
both app types. The backend still gates actual preservation on
can_preserve_on_behalf_of, so a non-deployer cannot escalate.

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

* fix(frontend): preserve on-behalf-of in the AI-chat raw-app deploy

The global AI-chat deploy path (`deploy_workspace_item` → createAppRaw/
updateAppRaw in copilot/chat/global/core.ts) carried the recomputed policy
forward but omitted preserve_on_behalf_of, so deploying a raw app via chat
reset the policy's on_behalf_of to the deploying user — the last of the
deploy surfaces with this gap. Send the flag when the policy has an
on_behalf_of, mirroring the editor and draft-deploy paths; the backend still
gates preservation on can_preserve_on_behalf_of.

Add a regression test asserting the flag is forwarded when the deployed
policy carries an on_behalf_of.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 17:10:21 +00:00

108 lines
4.2 KiB
TypeScript

/**
* Deploy a raw app (code-based app) from its server-side draft. Raw apps can't
* be deployed through the normal AppService.updateApp/createApp path: their
* source `files` must be bundled to js/css and saved via the raw-app endpoints.
*
* This mirrors how the global AI chat deploys raw apps
* (`copilot/chat/global/core.ts` → deployDraft, case 'app'): read the item with
* its draft, normalise to an AppDraftValue, recompute the policy, bundle the
* files, then createAppRaw/updateAppRaw. The source→AppDraftValue projection is
* shared via `rawAppDraftValue` so the two deploy paths can't drift.
*/
import { get } from 'svelte/store'
import { AppService } from '$lib/gen'
import type { Policy } from '$lib/gen'
import { userStore } from '$lib/stores'
import { bundleRawAppDraft } from '$lib/components/copilot/chat/global/rawAppBundlerBridge'
import type { AppDraftValue } from '$lib/components/copilot/chat/global/workspaceItems'
import { updateRawAppPolicy } from '$lib/components/raw_apps/rawAppPolicy'
import { DEFAULT_DATA as DEFAULT_RAW_APP_DATA } from '$lib/components/raw_apps/dataTableRefUtils'
import { appSourceToDraftValue } from '$lib/components/raw_apps/rawAppDraftValue'
/**
* Promote a raw app's draft to deployed. Throws on failure (caller wraps into a
* DeployResult). The matching draft row is deleted server-side by the raw-app
* create/update handler, like the other deploy paths.
*/
export async function deployRawAppDraft(
workspace: string,
path: string,
deploymentMessage?: string
): Promise<void> {
// `rawApp: true` so a never-deployed raw app (no `app` row) resolves to
// the raw_app draft kind server-side instead of 404ing.
const app = await AppService.getAppByPath({ workspace, path, getDraft: true, rawApp: true })
const draft = (app as any).draft
// Deploy at the draft's intended path. A raw-app draft carries the user-typed
// path in `draft_path` (a never-deployed app is parked at a synthetic
// `u/{user}/draft_{uuid}` storage key); the URL `path` below stays that storage
// key. Falls back to `path` for an unrenamed draft on a deployed app.
const targetPath = draft?.draft_path ?? draft?.path ?? path
const value = appSourceToDraftValue(draft ?? app, app)
const policy = (await updateRawAppPolicy(
value.runnables as any,
value.policy as any
)) as NonNullable<AppDraftValue['policy']> & Policy
if (!policy.execution_mode) {
policy.execution_mode = 'publisher'
}
const bundle = await bundleRawAppDraft({ workspace, files: value.files })
const rawAppValue = {
files: value.files,
runnables: value.runnables,
data: value.data ?? { ...DEFAULT_RAW_APP_DATA }
}
const summary = value.summary ?? ''
if (await AppService.existsApp({ workspace, path })) {
// custom_path changes require admin. Mirror RawAppEditorHeader's update path:
// admins send the draft's value (`''` to clear), non-admins send undefined so
// the backend ignores it and preserves the existing route — otherwise a
// non-admin deploying a draft for an app that has a custom route would hit
// RequireAdmin (the deployed custom_path is sent via the appSourceToDraftValue
// fallback even when unchanged).
const isAdmin = !!(get(userStore)?.is_admin || get(userStore)?.is_super_admin)
await AppService.updateAppRaw({
workspace,
path,
formData: {
app: {
path: targetPath,
value: rawAppValue,
summary,
policy,
deployment_message: deploymentMessage,
custom_path: isAdmin ? (value.custom_path ?? '') : undefined,
// Preserve the policy's on_behalf_of: this draft-deploy path has no
// on-behalf-of selector, so without the flag the backend resets it to
// the deploying user (gated server-side by can_preserve_on_behalf_of).
preserve_on_behalf_of: policy.on_behalf_of ? true : undefined
},
js: bundle.js,
css: bundle.css
}
})
} else {
await AppService.createAppRaw({
workspace,
formData: {
app: {
path: targetPath,
value: rawAppValue,
summary,
policy,
deployment_message: deploymentMessage,
custom_path: value.custom_path,
// Preserve the policy's on_behalf_of (see update branch above).
preserve_on_behalf_of: policy.on_behalf_of ? true : undefined
},
js: bundle.js,
css: bundle.css
}
})
}
}