Skip to content

Commit 52960ca

Browse files
windmill-internal-app[bot]windmill-internal-app[bot]rubenfiszel
authored
fix: reset parent_hash in auto_parent when all versions at path are archived (#9172)
* fix: reset parent_hash in auto_parent when all versions at path are archived * test: regression test for auto_parent with all versions archived --------- Co-authored-by: windmill-internal-app[bot] <1429786+windmill-internal-app[bot]@users.noreply.github.com> Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
1 parent 302ce58 commit 52960ca

2 files changed

Lines changed: 150 additions & 0 deletions

File tree

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
//! Regression test for `auto_parent` when all versions at a script path are
2+
//! archived (e.g. after a rename).
3+
//!
4+
//! The CLI's `wmill sync push` sends `parent_hash` together with
5+
//! `auto_parent: true`, delegating parent resolution to the backend. When every
6+
//! version at the target path is archived, there is no active head, so the
7+
//! stale `parent_hash` (an archived ancestor) used to leak into the lineage
8+
//! check and produce a spurious
9+
//! `lineage must be linear: no 2 scripts can have the same parent` error
10+
//! whenever that archived hash already had a child from the prior rename.
11+
//!
12+
//! The fix clears `parent_hash` to `None` in that case so the push starts a
13+
//! fresh lineage instead of failing.
14+
15+
use serde_json::json;
16+
use sqlx::{Pool, Postgres};
17+
use windmill_test_utils::*;
18+
19+
fn client() -> reqwest::Client {
20+
reqwest::Client::new()
21+
}
22+
23+
fn authed(builder: reqwest::RequestBuilder, token: &str) -> reqwest::RequestBuilder {
24+
builder.header("Authorization", format!("Bearer {}", token))
25+
}
26+
27+
fn new_script(path: &str, content: &str) -> serde_json::Value {
28+
json!({
29+
"path": path,
30+
"summary": "",
31+
"description": "",
32+
"content": content,
33+
"language": "deno",
34+
"schema": {
35+
"$schema": "https://json-schema.org/draft/2020-12/schema",
36+
"type": "object",
37+
"properties": {},
38+
"required": []
39+
}
40+
})
41+
}
42+
43+
#[sqlx::test(fixtures("base"))]
44+
async fn test_auto_parent_starts_fresh_lineage_when_all_versions_archived(
45+
db: Pool<Postgres>,
46+
) -> anyhow::Result<()> {
47+
initialize_tracing().await;
48+
49+
let server = ApiServer::start(db.clone()).await?;
50+
let port = server.addr.port();
51+
let base = format!("http://localhost:{port}/api/w/test-workspace");
52+
53+
let original_path = "u/test-user/script_archived_parent";
54+
let renamed_path = "u/test-user/script_renamed";
55+
56+
// 1. Create the initial version at the original path.
57+
let resp = authed(
58+
client().post(format!("{base}/scripts/create")),
59+
"SECRET_TOKEN",
60+
)
61+
.json(&new_script(
62+
original_path,
63+
"export async function main() { return 1; }",
64+
))
65+
.send()
66+
.await?;
67+
assert_eq!(resp.status(), 201);
68+
let original_hash: String = resp.text().await?;
69+
70+
// 2. Rename the script (new path, parent_hash pointing at v1). This
71+
// archives the original hash and gives the new version a
72+
// `parent_hashes[1]` equal to `original_hash`, so the original path now
73+
// has only archived versions.
74+
let mut rename = new_script(renamed_path, "export async function main() { return 2; }");
75+
rename["parent_hash"] = json!(original_hash);
76+
let resp = authed(
77+
client().post(format!("{base}/scripts/create")),
78+
"SECRET_TOKEN",
79+
)
80+
.json(&rename)
81+
.send()
82+
.await?;
83+
assert_eq!(
84+
resp.status(),
85+
201,
86+
"rename should succeed: {}",
87+
resp.text().await?
88+
);
89+
90+
// Sanity: the original path has no active (non-archived) version.
91+
let active_at_original: bool = sqlx::query_scalar(
92+
"SELECT EXISTS(SELECT 1 FROM script WHERE path = $1 AND archived = false AND workspace_id = $2)",
93+
)
94+
.bind(original_path)
95+
.bind("test-workspace")
96+
.fetch_one(&db)
97+
.await?;
98+
assert!(
99+
!active_at_original,
100+
"all versions at the original path should be archived after rename"
101+
);
102+
103+
// 3. Reproduce `wmill sync push`: push back to the original path with the
104+
// stale archived `parent_hash` AND `auto_parent: true`. Before the fix
105+
// this returned 400 "lineage must be linear" because the archived hash
106+
// already had a child (the renamed version) sharing the same parent.
107+
let mut push = new_script(original_path, "export async function main() { return 3; }");
108+
push["parent_hash"] = json!(original_hash);
109+
push["auto_parent"] = json!(true);
110+
let resp = authed(
111+
client().post(format!("{base}/scripts/create")),
112+
"SECRET_TOKEN",
113+
)
114+
.json(&push)
115+
.send()
116+
.await?;
117+
let status = resp.status();
118+
let body = resp.text().await?;
119+
assert_eq!(
120+
status, 201,
121+
"auto_parent push to a path with only archived versions should start a \
122+
fresh lineage, got {status}: {body}"
123+
);
124+
125+
// 4. There is now exactly one active version at the original path and it is
126+
// a fresh lineage with no parent (rather than attaching to the archived
127+
// ancestor).
128+
let active: Vec<Option<Vec<i64>>> = sqlx::query_scalar(
129+
"SELECT parent_hashes FROM script \
130+
WHERE path = $1 AND archived = false AND workspace_id = $2",
131+
)
132+
.bind(original_path)
133+
.bind("test-workspace")
134+
.fetch_all(&db)
135+
.await?;
136+
assert_eq!(
137+
active.len(),
138+
1,
139+
"exactly one active version expected at the original path"
140+
);
141+
assert!(
142+
active[0].is_none(),
143+
"fresh lineage should have no parent_hashes, got {:?}",
144+
active[0]
145+
);
146+
147+
Ok(())
148+
}

backend/windmill-api-scripts/src/scripts.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,8 @@ async fn create_script_internal<'c>(
997997
if ns.auto_parent.unwrap_or(false) {
998998
if let Some(ref cs) = clashing_script {
999999
ns.parent_hash = Some(cs.hash.clone());
1000+
} else {
1001+
ns.parent_hash = None;
10001002
}
10011003
}
10021004

0 commit comments

Comments
 (0)