mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
fix: a redeploy ends a route off its path, take-latest persists on raw apps, stale picker loads are dropped
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f9123297b8
commit
0a1dca929b
@@ -303,3 +303,36 @@ async fn test_an_owner_move_extends_an_item_move(db: Pool<Postgres>) -> anyhow::
|
||||
assert_eq!(own_draft_paths(port).await?, vec!["u/test-user/follow_c"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Redeploying at a path an owner's move routed away from ends that route: the live item
|
||||
/// owns its path again, and its saves must not follow the draft that left.
|
||||
#[sqlx::test(fixtures("base", "drafts_save_follows_move"))]
|
||||
async fn test_redeploy_at_a_routed_path_ends_the_route(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
// `move_draft` ignores archived rows, so an archived script's draft can be moved away.
|
||||
sqlx::query("UPDATE script SET archived = true WHERE path = 'u/test-user/follow_a'")
|
||||
.execute(&db)
|
||||
.await?;
|
||||
let resp = reqwest::Client::new()
|
||||
.post(format!(
|
||||
"http://localhost:{port}/api/w/test-workspace/drafts/move/script/u/test-user/follow_a"
|
||||
))
|
||||
.header("Authorization", "Bearer SECRET_TOKEN")
|
||||
.json(&json!({ "new_path": "u/test-user/follow_b" }))
|
||||
.send()
|
||||
.await?;
|
||||
assert!(resp.status().is_success(), "move failed: {}", resp.text().await?);
|
||||
|
||||
// Unarchiving redeploys at the same path, with the archived version as parent.
|
||||
rename(port, HEAD_HASH, "u/test-user/follow_a").await?;
|
||||
|
||||
assert_eq!(
|
||||
save_at(port, "u/test-user/follow_a", "for the live script").await?,
|
||||
"u/test-user/follow_a",
|
||||
"a save for the redeployed script followed the draft that moved away"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2417,14 +2417,17 @@ async fn create_script_internal<'c>(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
windmill_common::user_drafts::clear_draft_moves_from(
|
||||
&mut tx,
|
||||
&w_id,
|
||||
&[UserDraftItemKind::Script],
|
||||
&ns.path,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
// Every deploy, not only a new script: an archived script's draft can be moved away
|
||||
// (`move_draft` ignores archived rows), and unarchiving redeploys at the same path,
|
||||
// where a route left behind would send the live script's saves to the moved draft.
|
||||
windmill_common::user_drafts::clear_draft_moves_from(
|
||||
&mut tx,
|
||||
&w_id,
|
||||
&[UserDraftItemKind::Script],
|
||||
&ns.path,
|
||||
)
|
||||
.await?;
|
||||
if p_hashes.is_some() && !p_hashes.unwrap().is_empty() {
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
|
||||
@@ -748,8 +748,8 @@ pub async fn move_drafts_for_path(
|
||||
/// record to one user's draft-only move; `None` is a deployed item's move, for everyone.
|
||||
///
|
||||
/// Kept to one hop: records pointing at `old_path` are re-pointed (an owner's move
|
||||
/// copies another scope's into its own rather than re-pointing it), and records
|
||||
/// leaving either path are replaced, since `new_path` now holds the item.
|
||||
/// leaves the item's own record alone and copies it into its own scope instead), and
|
||||
/// records leaving either path are replaced, since `new_path` now holds the item.
|
||||
///
|
||||
/// **The caller must have authorized the move first.** A record routes every later
|
||||
/// draft write at `old_path` (any owner's, for an item move), and enforces nothing
|
||||
|
||||
@@ -101,6 +101,9 @@
|
||||
loadingVersion = true
|
||||
try {
|
||||
const value = await versionLoader(id)
|
||||
// A slower earlier request must not replace what the picker now shows, nor
|
||||
// clear the spinner the newer one is still running under.
|
||||
if (selectedVersion !== id) return
|
||||
if (!value || !data || data.mode !== 'normal') return
|
||||
const opt = data.versions?.find((v) => v.id === id)
|
||||
data = {
|
||||
@@ -109,7 +112,7 @@
|
||||
deployedLabel: opt?.isHead ? headLabel : opt?.label
|
||||
}
|
||||
} finally {
|
||||
loadingVersion = false
|
||||
if (selectedVersion === id) loadingVersion = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -628,12 +628,22 @@
|
||||
onTakeLatest={draftBaseVersion &&
|
||||
deployedHeadVersion &&
|
||||
draftBaseVersion !== deployedHeadVersion
|
||||
? () => {
|
||||
? async () => {
|
||||
const head = Number(deployedHeadVersion)
|
||||
// The bundle carries `parentVersion`, so this alone re-persists the draft.
|
||||
parentVersion = head
|
||||
if (deployedBaseline) deployedBaseline = { ...deployedBaseline, parent_version: head }
|
||||
draftBaseVersion = String(head)
|
||||
// Persisted explicitly, as the script and flow routes do: the reactive
|
||||
// bundle mirror is parked while auto-save is off, and a parked write is
|
||||
// dropped on pagehide, so the new base would not survive a reload.
|
||||
if (draftSync.draft) {
|
||||
draftSync.draft = { ...draftSync.draft, parent_version: head }
|
||||
if ($workspaceStore) {
|
||||
await UserDraft.forcePersist('raw_app', page.params.path ?? '', {
|
||||
workspace: $workspaceStore
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
: undefined}
|
||||
onDeploy={({ version }) => {
|
||||
|
||||
Reference in New Issue
Block a user