feat: restart perpetual runs on the version a deploy makes runnable (#11200)

* feat: opt-in move of perpetual runs to a newly deployed script version

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: keep the perpetual-run opt-in across relocks and check the new version's tag

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: pin the tag check on a perpetual version switch

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: count perpetual runs past the first queue page in the deploy prompt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: restart perpetual runs on the version a deploy makes runnable

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: claim a perpetual run and queue its replacement in one transaction

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: honour a cancel that lands after the worker last read its queue row

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor: leave the lost-cancel fix to its own PR and match the scale down to 0 wording

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: run a preprocessor the deployed version adds over the arguments carried over

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* style: shorten the wording of the modal's argument warning

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor: never preprocess a restarted perpetual run, as every other restart does

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: preprocess for a replacement whose run had not been through one

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: push a perpetual replacement without the deployed debounce settings

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: name the runs the deploy button restarts

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* perf: skip the perpetual restart lookups on a deploy that is not perpetual

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: move perpetual runs before anything that can fail after the deploy commits

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: report cancellation on the queue listing so the deploy prompt can skip it

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: pass the tag workspace to the availability check after the merge

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* style: say which arguments are defined differently and which values are kept

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: resolve a dynamic tag before checking it for a restarted run

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: pin that a deployed dynamic tag is checked as it resolves

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-09-29 18:03:43 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent bda00c6cad
commit cf5c49c3dc
11 changed files with 1056 additions and 4 deletions
@@ -551,6 +551,7 @@ async fn create_snapshot_script(
let mut handle_deployment_metadata = None;
let mut moved_native_triggers = Vec::new();
let mut deployed_path = None;
let mut deployed_perpetual = false;
while let Some(field) = multipart.next_field().await.unwrap() {
let name = field.name().unwrap().to_string();
let data = field.bytes().await.unwrap();
@@ -559,6 +560,7 @@ async fn create_snapshot_script(
let is_tar = ns.codebase.as_ref().is_some_and(|x| x.ends_with(".tar"));
let use_esm = ns.codebase.as_ref().is_some_and(|x| x.contains(".esm"));
deployed_path = Some(ns.path.clone());
deployed_perpetual = ns.restart_unless_cancelled == Some(true);
let (new_hash, ntx, hdm, moved) = create_script_internal(
ns,
w_id.clone(),
@@ -616,6 +618,19 @@ async fn create_snapshot_script(
}
reregister_moved_native_triggers(&db, &authed, &w_id, moved_native_triggers);
if let Some(hdm) = handle_deployment_metadata {
let runnable_now = matches!(hdm, PostCommitDeploy::Full { .. });
if let Some(script_path) = deployed_path
.as_deref()
.filter(|_| runnable_now && deployed_perpetual)
{
windmill_queue::restart_perpetual_runs_on_new_version(
&db,
&w_id,
script_path,
&authed.username,
)
.await;
}
hdm.handle(&db).await?;
}
return Ok((StatusCode::CREATED, format!("{}", script_hash.unwrap())));
@@ -734,6 +749,9 @@ async fn deploy_script(
return Err(Error::PermissionDenied(msg));
}
let script_path = ns.path.clone();
// Only a perpetual deploy can have runs to move, so every other one skips the lookups that
// would find that out.
let perpetual = ns.restart_unless_cancelled == Some(true);
let email = authed.email.clone();
let username = authed.username.clone();
let authed_for_triggers = authed.clone();
@@ -757,6 +775,19 @@ async fn deploy_script(
// they don't run against a version whose lock does not exist yet — and
// don't run twice.
let ready_to_test = matches!(hdm, PostCommitDeploy::Full { .. });
// The version is runnable, so the perpetual runs of earlier ones move to it here, before
// anything that can fail this deploy after its commit: a version nothing moved to would
// leave those runs on the old code with nothing left to notice. A deploy that needed lock
// generation hands this to its dependency job instead.
if ready_to_test && perpetual {
windmill_queue::restart_perpetual_runs_on_new_version(
&db,
&w_id,
&script_path,
&username,
)
.await;
}
hdm.handle(&db).await?;
let db2 = db.clone();
if ready_to_test {
@@ -2864,6 +2895,12 @@ async fn create_script_internal<'c>(
if let Some(dm) = ns.deployment_message {
args.insert("deployment_message".to_string(), to_raw_value(&dm));
}
// The version becomes runnable when this job writes its lock, which is where the
// perpetual runs of earlier versions can move to it. Only a deploy someone made carries
// this, so a relock triggered by an imported script changing leaves those runs alone.
if ns.restart_unless_cancelled.is_some_and(|x| x) {
args.insert("restart_perpetual_runs".to_string(), to_raw_value(&true));
}
if let Some(ref p_path) = p_path_opt {
args.insert("parent_path".to_string(), to_raw_value(&p_path));
}