mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 00:02:19 +00:00
c57c769dea
* feat: add CI test scripts with auto-trigger on deploy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: fix annotation parser early return and handle renames correctly Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: move CI test results to top of script/flow detail pages Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: improve CI test results spacing, icon, and remove pass label Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: support one-line annotation and use script/path format Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: move CI test trigger logic to EE Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: move CI badge next to New badge and add deduplicated CI summary Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add CI test e2e tests and fix nullable column annotations Add integration tests for CI test annotation parsing (creates/removes ci_test_reference rows) and the CI test results API (single + batch endpoints). Add backend test for auto-trigger on deploy (private+python). Fix sqlx LEFT JOIN LATERAL nullable column annotations in get_ci_test_results and get_ci_test_results_batch queries — sqlx cannot infer nullability from LATERAL subqueries, causing runtime decode errors when no matching job exists. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix build/sqlx * fix * feat: CI test improvements and templates - Fix windmill-dep-map/private feature propagation in worker, api-scripts, and api-flows Cargo.toml so CI test triggers actually fire in EE mode - Clone ci_test_reference rows during workspace fork - Add polling to CiTestResults component (refetch every 3s while running) - Add running state and auto-refresh to ForkWorkspaceBanner CI summary - Add yellow "CI test" badge on script list rows and detail page - Fix Library badge border color (remove indigo border override) - Add CI Test TypeScript and CI Test Python templates in ScriptBuilder - Update sqlx offline cache - Add debug tracing for CI test trigger in worker_lockfiles Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add missing children prop to WorkspaceDeployLayout Fixes svelte-fast-check type error when passing named snippets as children content inside the component tag. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review feedback - Remove empty wrapper divs around CiTestResults, move mb-4 into component - Add batch endpoint size cap (max 200 items) - Add ON DELETE CASCADE to ci_test_reference workspace FK (new migration) - Downgrade CI test trigger logs from info to debug - Fix false-positive polling: only treat status='running' as running, not null status (CiTestResults, CompareWorkspaces, ForkWorkspaceBanner) - Fix test numbering in integration tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to latest EE commit Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to d9d68c2406df0b59f413ea0b2cb24780a9817d04 This commit updates the EE repository reference after PR #516 was merged in windmill-ee-private. Previous ee-repo-ref: d7ccd9b86da99ec056a0e8708e3637d64290387a New ee-repo-ref: d9d68c2406df0b59f413ea0b2cb24780a9817d04 Automated by sync-ee-ref workflow. * fix: treat queued jobs (job_id set, null status) as running Jobs that have been pushed but not yet picked up by a worker have a job_id but null status. Treat these as 'running' to avoid showing misleading 'pass' badges or '0 passing'. Tests that were never triggered (no job_id, null status) remain neutral/hidden. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: hugocasa <hugo@casademont.ch> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
213 lines
6.4 KiB
Rust
213 lines
6.4 KiB
Rust
pub mod ci_tests;
|
|
#[cfg(feature = "private")]
|
|
pub mod ci_tests_ee;
|
|
pub mod scoped_dependency_map;
|
|
pub mod trigger_dependents;
|
|
pub mod workspace_dependencies;
|
|
|
|
use std::collections::HashMap;
|
|
use std::path::{Component, Path, PathBuf};
|
|
|
|
use serde_json::value::RawValue;
|
|
use sqlx::types::Json;
|
|
use uuid::Uuid;
|
|
use windmill_common::error;
|
|
use windmill_common::scripts::ScriptLang;
|
|
use windmill_common::utils::WarnAfterExt;
|
|
use windmill_common::workspace_dependencies::{
|
|
WorkspaceDependencies, WorkspaceDependenciesPrefetched,
|
|
};
|
|
use windmill_parser_ts::parse_expr_for_imports;
|
|
|
|
fn try_normalize(path: &Path) -> Option<PathBuf> {
|
|
let mut ret = PathBuf::new();
|
|
|
|
for component in path.components() {
|
|
match component {
|
|
Component::Prefix(..) | Component::RootDir => return None,
|
|
Component::CurDir => {}
|
|
Component::ParentDir => {
|
|
if !ret.pop() {
|
|
return None;
|
|
}
|
|
}
|
|
Component::Normal(c) => {
|
|
ret.push(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
Some(ret)
|
|
}
|
|
|
|
fn parse_ts_relative_imports(
|
|
raw_code: &str,
|
|
script_path: &str,
|
|
) -> windmill_common::error::Result<Vec<String>> {
|
|
let mut relative_imports = vec![];
|
|
let r = parse_expr_for_imports(raw_code, true)?;
|
|
for import in r {
|
|
let import = import.trim_end_matches(".ts");
|
|
if import.starts_with("/") {
|
|
relative_imports.push(import.trim_start_matches("/").to_string());
|
|
} else if import.starts_with(".") {
|
|
let normalized = try_normalize(std::path::Path::new(&format!(
|
|
"{}/../{}",
|
|
script_path, import
|
|
)));
|
|
if let Some(normalized) = normalized {
|
|
let normalized = normalized.to_str().unwrap().to_string();
|
|
relative_imports.push(normalized);
|
|
} else {
|
|
tracing::error!("error canonicalizing path: {script_path} with import {import}");
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(relative_imports)
|
|
}
|
|
|
|
pub fn extract_relative_imports(
|
|
raw_code: &str,
|
|
script_path: &str,
|
|
language: &Option<ScriptLang>,
|
|
) -> Option<Vec<String>> {
|
|
match language {
|
|
#[cfg(feature = "python")]
|
|
Some(ScriptLang::Python3) => {
|
|
windmill_parser_py_imports::parse_relative_imports(&raw_code, script_path).ok()
|
|
}
|
|
Some(ScriptLang::Bun) | Some(ScriptLang::Bunnative) | Some(ScriptLang::Deno) => {
|
|
parse_ts_relative_imports(&raw_code, script_path).ok()
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn extract_referenced_paths(
|
|
raw_code: &str,
|
|
script_path: &str,
|
|
language: Option<ScriptLang>,
|
|
) -> Option<Vec<String>> {
|
|
let mut referenced_paths = vec![];
|
|
if let Some(wk_deps_refs) = language
|
|
.and_then(|l| {
|
|
windmill_common::scripts::extract_workspace_dependencies_annotated_refs(
|
|
&l,
|
|
raw_code,
|
|
script_path,
|
|
)
|
|
})
|
|
.map(|r| r.external)
|
|
{
|
|
let l = language.expect("should be some");
|
|
for wk_deps_ref in wk_deps_refs {
|
|
if let Some(path) = WorkspaceDependencies::to_path(&Some(wk_deps_ref), l).ok() {
|
|
referenced_paths.push(path);
|
|
};
|
|
}
|
|
} else if let (Some(l), true /* Only if it is not blacklisted */) = (
|
|
language,
|
|
WorkspaceDependenciesPrefetched::is_external_references_permitted(script_path),
|
|
) {
|
|
// we assume all runnables without annotated dependencies reference default dependencies file.
|
|
WorkspaceDependencies::to_path(&None, l)
|
|
.ok()
|
|
.inspect(|p| referenced_paths.push(p.to_owned()));
|
|
}
|
|
|
|
if let Some(relative_imports) = extract_relative_imports(raw_code, script_path, &language) {
|
|
referenced_paths.extend(relative_imports);
|
|
}
|
|
|
|
if referenced_paths.is_empty() {
|
|
None
|
|
} else {
|
|
Some(referenced_paths)
|
|
}
|
|
}
|
|
|
|
pub async fn process_relative_imports(
|
|
db: &sqlx::Pool<sqlx::Postgres>,
|
|
_job_id: Option<Uuid>,
|
|
args: Option<&Json<HashMap<String, Box<RawValue>>>>,
|
|
w_id: &str,
|
|
script_path: &str,
|
|
parent_path: Option<String>,
|
|
deployment_message: Option<String>,
|
|
code: &str,
|
|
script_lang: &Option<ScriptLang>,
|
|
permissioned_as_email: &str,
|
|
created_by: &str,
|
|
permissioned_as: &str,
|
|
) -> error::Result<()> {
|
|
use scoped_dependency_map::ScopedDependencyMap;
|
|
use trigger_dependents::trigger_dependents_to_recompute_dependencies;
|
|
|
|
// TODO: Should be moved into handle_dependency_job body to be more consistent with how flows and apps are handled
|
|
{
|
|
let mut tx = db.begin().await?;
|
|
let mut dependency_map = ScopedDependencyMap::fetch_maybe_rearranged(
|
|
&w_id,
|
|
script_path,
|
|
"script",
|
|
&parent_path,
|
|
db,
|
|
)
|
|
.await?;
|
|
|
|
tx = dependency_map
|
|
.patch(
|
|
extract_referenced_paths(&code, script_path, *script_lang),
|
|
// Ideally should be None, but due to current implementation will use empty string to represent None.
|
|
"".into(),
|
|
tx,
|
|
)
|
|
.await?;
|
|
|
|
dependency_map.dissolve(tx).await.commit().await?;
|
|
}
|
|
|
|
{
|
|
let mut already_visited = args
|
|
.map(|x| {
|
|
x.get("already_visited")
|
|
.map(|v| serde_json::from_str::<Vec<String>>(v.get()).ok())
|
|
.flatten()
|
|
})
|
|
.flatten()
|
|
.unwrap_or_default();
|
|
|
|
let importers = ScopedDependencyMap::get_dependents(script_path, w_id, db).await?;
|
|
|
|
already_visited.push(script_path.to_string());
|
|
match tokio::time::timeout(
|
|
core::time::Duration::from_secs(60),
|
|
Box::pin(trigger_dependents_to_recompute_dependencies(
|
|
w_id,
|
|
importers,
|
|
deployment_message,
|
|
parent_path,
|
|
permissioned_as_email,
|
|
created_by,
|
|
permissioned_as,
|
|
db,
|
|
already_visited,
|
|
)),
|
|
)
|
|
.warn_after_seconds(10)
|
|
.await
|
|
{
|
|
Ok(Err(e)) => {
|
|
tracing::error!(%e, "error triggering dependents to recompute dependencies")
|
|
}
|
|
Err(e) => {
|
|
tracing::error!(%e, "triggering dependents to recompute dependencies has timed out")
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|