diff --git a/cli/src/commands/app/app.ts b/cli/src/commands/app/app.ts index 470f0f4b11..0060c2c422 100644 --- a/cli/src/commands/app/app.ts +++ b/cli/src/commands/app/app.ts @@ -115,6 +115,14 @@ export function replaceInlineScripts( } return []; } +/** `parent_version` is the editor's stale-draft base: a version id that changes on + * every deploy and means nothing in another workspace, so it is kept out of the + * tracked file and out of what a push deploys. */ +export function stripEditorState(app: any) { + if (app?.value && typeof app.value == "object") { + delete app.value.parent_version; + } +} export function isExecutionModeAnonymous(app: any) { return app?.["policy"]?.["execution_mode"] == "anonymous"; } @@ -294,6 +302,7 @@ export async function pushApp( } const path = localPath + "app.yaml"; const localApp = (await yamlParseFile(path)) as AppFile; + stripEditorState(localApp); const isolation = statedIsolation(localApp, false); replaceInlineScripts(localApp.value, localPath, true); diff --git a/cli/src/commands/sync/sync.ts b/cli/src/commands/sync/sync.ts index b9e373abad..5ba8888332 100644 --- a/cli/src/commands/sync/sync.ts +++ b/cli/src/commands/sync/sync.ts @@ -149,7 +149,11 @@ import { extractCurrentMapping, } from "../../../windmill-utils-internal/src/inline-scripts/extractor.ts"; import { generateFlowLockInternal } from "../flow/flow_metadata.ts"; -import { markAccessFromPolicy, markIsolationFromPolicy } from "../app/app.ts"; +import { + markAccessFromPolicy, + markIsolationFromPolicy, + stripEditorState, +} from "../app/app.ts"; import { APP_BACKEND_FOLDER, generateAppLocksInternal, @@ -1329,6 +1333,7 @@ export function ZipFSElement( log.error(`Failed to parse app.yaml at path: ${p}`); throw error; } + stripEditorState(app); let inlineScripts; try { inlineScripts = extractInlineScriptsForApps( @@ -2749,12 +2754,18 @@ export async function compareDynFSElement( } return o; } else { + let o: any; try { - return yamlParseContent(k, v); + o = yamlParseContent(k, v); } catch (error) { log.error(`Failed to parse YAML content at path: ${k}`); throw error; } + // A file tracked before the field was dropped still carries it. + if (path.basename(k) == "app.yaml") { + stripEditorState(o); + } + return o; } } diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte index f07d596baa..9fd083a75d 100644 --- a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte @@ -284,13 +284,21 @@ saveDrawerOpen = false } + // `parent_version` is the draft's stale-check base. Deployed, it would put a version + // id that changes on every deploy into git-synced files and version diffs. + function withoutDraftBase(a: T): T { + if (!a) return a + const { parent_version: _pv, ...rest } = a + return rest as T + } + async function createApp(path: string) { policy = await updatePolicy($app, policy) try { await AppService.createApp({ workspace: $workspaceStore!, requestBody: { - value: $app, + value: withoutDraftBase($app), path, summary: $summary, policy, @@ -349,7 +357,7 @@ orderedJsonStringify( replaceFalseWithUndefined({ summary: $summary, - value: $app, + value: withoutDraftBase($app), path: newEditedPath || savedApp.path, policy, custom_path: customPath, @@ -392,6 +400,7 @@ // Strip off extra information deployedValue = replaceFalseWithUndefined({ ...deployedApp, + value: withoutDraftBase(deployedApp.value as App), id: undefined, created_at: undefined, created_by: undefined, @@ -406,7 +415,7 @@ workspace: $workspaceStore!, path: $appPath!, requestBody: { - value: $app!, + value: withoutDraftBase($app!), summary: $summary, policy, path: npath, @@ -753,7 +762,7 @@ deployed: deployedValue ?? savedApp, current: { summary: $summary, - value: $app, + value: withoutDraftBase($app), path: newEditedPath || savedApp.path, policy, custom_path: customPath, @@ -847,7 +856,7 @@ bind:deployedValue currentValue={{ summary: $summary, - value: $app, + value: withoutDraftBase($app), path: newEditedPath || savedApp?.path, policy, custom_path: customPath, @@ -898,7 +907,7 @@ deployed: deployedValue ?? savedApp, current: { summary: $summary, - value: $app, + value: withoutDraftBase($app), path: newEditedPath || savedApp.path, policy, custom_path: customPath, diff --git a/frontend/src/lib/utils_draft_deploy.ts b/frontend/src/lib/utils_draft_deploy.ts index df04bb383a..611d5b96e8 100644 --- a/frontend/src/lib/utils_draft_deploy.ts +++ b/frontend/src/lib/utils_draft_deploy.ts @@ -538,8 +538,14 @@ export async function deployDraft( // no draft. `draft_path` and `summary` are draft-only fields mirrored onto // the App value (the editor drops them on deploy), so strip them from the // value and apply them as the deploy path / summary column. + // `parent_version` is the draft's stale-check base, never deployed. const draft = r.draft as Record | undefined - const { draft_path: draftPath, summary: draftSummary, ...appValue } = draft ?? r.value ?? {} + const { + draft_path: draftPath, + summary: draftSummary, + parent_version: _pv, + ...appValue + } = draft ?? r.value ?? {} // Policy isn't carried in the app draft, so it comes from the deployed app // (or a default). custom_path requires admin on update; non-admins send // undefined so the backend preserves the existing route. The draft has no