fix: an app deploy pins only a version it wrote as the next draft's base

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-09-15 14:21:27 +02:00
co-authored by Claude Opus 5
parent a149ba9231
commit 03ec3fde2f
4 changed files with 46 additions and 7 deletions
@@ -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
@@ -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
}
@@ -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)
})
})
@@ -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')