diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte index 6b4481c826..1085fc06e8 100644 --- a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte @@ -63,7 +63,7 @@ import LazyModePanel from './contextPanel/LazyModePanel.svelte' import type { DiffDrawerI } from '$lib/components/diff_drawer' import AppEditorHeaderDeploy from './AppEditorHeaderDeploy.svelte' - import { computeSecretUrl } from './appDeploy.svelte' + import { computeSecretUrl, versionThisDeployWrote } from './appDeploy.svelte' import { updatePolicy } from './appPolicy' import { editInForkAllowed, editInForkLabel, openEditInFork } from '$lib/utils/editInFork' import { isCloudHosted } from '$lib/cloud' @@ -392,7 +392,7 @@ workspace: $workspaceStore!, path: npath }) - version = appHistory[0]?.version + version = versionThisDeployWrote(appHistory, $userStore?.username) // Re-pin the fork base to the just-deployed head: the editor stays open, so a // follow-up deploy (or a new edit) would otherwise compare against the now- // superseded base and falsely warn. parent_version is in diff --git a/frontend/src/lib/components/apps/editor/appDeploy.svelte.ts b/frontend/src/lib/components/apps/editor/appDeploy.svelte.ts index 122c42bcf9..b071166dee 100644 --- a/frontend/src/lib/components/apps/editor/appDeploy.svelte.ts +++ b/frontend/src/lib/components/apps/editor/appDeploy.svelte.ts @@ -1,7 +1,28 @@ -import { base } from "$lib/base" -import { workspaceStore } from "$lib/stores" -import { get } from "svelte/store" +import { base } from '$lib/base' +import { workspaceStore } from '$lib/stores' +import { get } from 'svelte/store' export function computeSecretUrl(secretUrl: string) { - return `${window.location.origin}${base}/public/${get(workspaceStore)}/${secretUrl}` + return `${window.location.origin}${base}/public/${get(workspaceStore)}/${secretUrl}` +} + +/** + * The version a just-finished deploy wrote, read back from the history it lands in. + * The deploy and this read are two requests, so someone else's deploy in between is the + * newest entry too; taking it would make their version the fork base of content it never + * contained, and the next deploy would find base === head and overwrite them with no + * warning. A head this caller did not write therefore yields `undefined`: an unknown + * base, which the timestamps still cover, rather than a wrong one. + * + * Author is all the history can be matched on (`deployment_msg` is written later, by the + * dependency job), so a second deploy by the same user inside that window still reads as + * this one's. + */ +export function versionThisDeployWrote( + history: { version: number; created_by?: string }[] | undefined, + deployedBy: string | undefined +): number | undefined { + const head = history?.[0] + if (!head || !deployedBy || head.created_by !== deployedBy) return undefined + return head.version } diff --git a/frontend/src/lib/components/apps/editor/appDeploy.test.ts b/frontend/src/lib/components/apps/editor/appDeploy.test.ts new file mode 100644 index 0000000000..184df32a96 --- /dev/null +++ b/frontend/src/lib/components/apps/editor/appDeploy.test.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from 'vitest' +import { versionThisDeployWrote } from './appDeploy.svelte' + +describe('versionThisDeployWrote', () => { + it('takes the head this deploy wrote', () => { + expect(versionThisDeployWrote([{ version: 7, created_by: 'alice' }], 'alice')).toBe(7) + }) + + it('claims nothing when another deploy landed on top', () => { + // The base would otherwise be a version this content never contained, and the + // next deploy would find base === head and overwrite it unwarned. + expect(versionThisDeployWrote([{ version: 8, created_by: 'bob' }], 'alice')).toBe(undefined) + expect(versionThisDeployWrote([], 'alice')).toBe(undefined) + expect(versionThisDeployWrote([{ version: 7, created_by: 'alice' }], undefined)).toBe(undefined) + }) +}) diff --git a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte index 538f433dbb..58c13fd88c 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte @@ -70,6 +70,7 @@ // `runtime.syncPreviewWithDeployed`, which discards the fork draft + reloads // the preview to the deployed version. import { AIBtnClasses } from '../copilot/chat/AIButtonStyle' + import { versionThisDeployWrote } from '../apps/editor/appDeploy.svelte' import { stripRawAppDiffNoise } from './utils' import type { RawAppData } from './dataTableRefUtils' import { editInForkAllowed, editInForkLabel, openEditInFork } from '$lib/utils/editInFork' @@ -544,7 +545,8 @@ workspace: opWorkspace!, path: npath }) - version = appHistory[0]?.version + // Only a version this deploy can claim becomes the draft's base below. + version = versionThisDeployWrote(appHistory, $userStore?.username) closeSaveDrawer() sendUserToast('App deployed successfully')