mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
fix: bundle deployed bun scripts whose only pin is on a dynamic import (#11096)
* fix: bundle deployed bun scripts whose only pin is on a dynamic import Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EH42obCk6WJnc7N4Fa25JH * fix: retry the no-db prebundle too, and guard bundles bun builds as written Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EH42obCk6WJnc7N4Fa25JH * fix: name the bundle retry after the import specifiers it unpins Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EH42obCk6WJnc7N4Fa25JH --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9fc50a23fb
commit
4afb9aa677
@@ -990,6 +990,76 @@ export function main() { return [ns === isNumber, label, local()]; }"#
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn bun_dependency_lock(db: &Pool<Postgres>, port: u16, path: &str, content: &str) -> String {
|
||||
let deps = RunJob::from(JobPayload::RawScriptDependencies {
|
||||
script_path: path.into(),
|
||||
content: content.into(),
|
||||
language: ScriptLang::Bun,
|
||||
})
|
||||
.run_until_complete(db, false, port)
|
||||
.await
|
||||
.json_result()
|
||||
.unwrap();
|
||||
let Some(lock) = deps["lock"].as_str() else {
|
||||
panic!("the dependency job returned no lock: {deps}");
|
||||
};
|
||||
lock.to_string()
|
||||
}
|
||||
|
||||
/// Bundling a locked script resolves a pinned dynamic `import()` as written. Where that fails, both
|
||||
/// the dependency job and a run that finds no cached bundle must still build it, from the version
|
||||
/// the lock pins; where bun tolerates the failure, the bundle must stay as written.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_bun_bundles_pinned_dynamic_import(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
// The dependency job saves the script's bundle here; the server binary creates it at startup.
|
||||
std::fs::create_dir_all(&*windmill_worker::BUN_BUNDLE_CACHE_DIR)?;
|
||||
|
||||
const PATH: &str = "f/pinned_dynamic_import/main";
|
||||
// Every `script` call draws its own nonce, so each job below misses every bundle cached before
|
||||
// it, this test's included, and has to build one: a cached bundle skips the build under test.
|
||||
// 4.17.20 is not npm's `latest`, so a bundle that lost the pin cannot match by accident.
|
||||
let script = |body: &str| {
|
||||
format!(
|
||||
"export async function main() {{\n {body}\n}}\n// {}",
|
||||
Uuid::new_v4()
|
||||
)
|
||||
};
|
||||
let import = r#"const m = await import("lodash@4.17.20"); return (m.default ?? m).VERSION;"#;
|
||||
|
||||
let lock = bun_dependency_lock(&db, port, PATH, &script(import)).await;
|
||||
let result = RunJob::from(JobPayload::Code(RawCode {
|
||||
content: script(import),
|
||||
path: Some(PATH.into()),
|
||||
language: ScriptLang::Bun,
|
||||
lock: Some(lock),
|
||||
..RawCode::default()
|
||||
}))
|
||||
.run_until_complete(&db, false, port)
|
||||
.await
|
||||
.json_result()
|
||||
.unwrap();
|
||||
assert_eq!(result, serde_json::json!("4.17.20"));
|
||||
|
||||
let tolerated = script(&format!("try {{ {import} }} catch {{ return null; }}"));
|
||||
let lock = bun_dependency_lock(&db, port, PATH, &tolerated).await;
|
||||
let (bundle, _) = windmill_worker::compute_bundle_local_and_remote_path(
|
||||
&tolerated,
|
||||
&lock,
|
||||
PATH,
|
||||
Some(&db),
|
||||
"test-workspace",
|
||||
&None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert!(std::fs::read_to_string(bundle)?.contains("lodash@4.17.20"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base", "bun_edge_cases"))]
|
||||
async fn test_bun_shared_imports_both_styles(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
@@ -1146,6 +1146,81 @@ pub async fn generate_bun_bundle(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// [`generate_bun_bundle`], built once more with the version pins dropped from the import
|
||||
/// specifiers of `main.ts` if it fails. The lockfile pins those versions, but bun fails on a
|
||||
/// pinned specifier except where it tolerates a failed import (in a `try`, under a `.catch`, in
|
||||
/// dead code). Such a script builds as written and must keep that bundle, so only failures retry.
|
||||
async fn generate_bun_bundle_unpinning_imports(
|
||||
job_dir: &str,
|
||||
w_id: &str,
|
||||
job_id: &Uuid,
|
||||
worker_name: &str,
|
||||
db: Option<&Connection>,
|
||||
timeout: Option<i32>,
|
||||
mem_peak: &mut i32,
|
||||
canceled_by: &mut Option<CanceledBy>,
|
||||
common_bun_proc_envs: &HashMap<String, String>,
|
||||
occupancy_metrics: &mut Option<&mut OccupancyMetrics>,
|
||||
) -> Result<()> {
|
||||
let built = generate_bun_bundle(
|
||||
job_dir,
|
||||
w_id,
|
||||
job_id,
|
||||
worker_name,
|
||||
db,
|
||||
timeout,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
common_bun_proc_envs,
|
||||
occupancy_metrics,
|
||||
)
|
||||
.await;
|
||||
// Without a job, a failed build comes back as an `ExecutionErr`; with one, that variant is a
|
||||
// cancellation or timeout, which must not be retried.
|
||||
let build_failed = match &built {
|
||||
Err(error::Error::ExitStatus(..)) => true,
|
||||
Err(_) => db.is_none(),
|
||||
Ok(()) => false,
|
||||
};
|
||||
if !build_failed {
|
||||
return built;
|
||||
}
|
||||
let Some(unpinned) = read_file_content(&format!("{job_dir}/main.ts"))
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|main| {
|
||||
remove_pinned_import_specifiers(&main)
|
||||
.ok()
|
||||
.filter(|u| *u != main)
|
||||
})
|
||||
else {
|
||||
return built;
|
||||
};
|
||||
write_file(job_dir, "main.ts", &unpinned)?;
|
||||
if let Some(db) = db {
|
||||
append_logs(
|
||||
job_id,
|
||||
w_id,
|
||||
"\nbundling again with the imports' versions taken from the lockfile\n",
|
||||
db,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
generate_bun_bundle(
|
||||
job_dir,
|
||||
w_id,
|
||||
job_id,
|
||||
worker_name,
|
||||
db,
|
||||
timeout,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
common_bun_proc_envs,
|
||||
occupancy_metrics,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
struct PulledCodebase {
|
||||
is_esm: bool,
|
||||
}
|
||||
@@ -1305,7 +1380,7 @@ pub async fn prebundle_bun_script(
|
||||
|
||||
let common_bun_proc_envs: HashMap<String, String> = get_common_bun_proc_envs(None).await;
|
||||
|
||||
generate_bun_bundle(
|
||||
generate_bun_bundle_unpinning_imports(
|
||||
job_dir,
|
||||
w_id,
|
||||
job_id,
|
||||
@@ -2202,7 +2277,7 @@ try {{
|
||||
|
||||
if !codebase.is_some() && !has_bundle_cache {
|
||||
if build_cache {
|
||||
generate_bun_bundle(
|
||||
generate_bun_bundle_unpinning_imports(
|
||||
job_dir,
|
||||
&job.workspace_id,
|
||||
&job.id,
|
||||
|
||||
Reference in New Issue
Block a user