mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-26 00:01:37 +00:00
feat(forks): strip operational state from triggers/schedules on git-sync export
When the source workspace is a fork (`wm-fork-*`), the tarball export now omits `mode` from triggers and `enabled` from schedules. The trigger update handler also preserves the existing DB `mode` when both fields are absent from the request, instead of falling back to the BaseTriggerData default. This prevents a fork's git-sync round-trip from flipping the parent workspace's enabled/disabled state when a merge applies the fork's YAML back to main. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,7 @@ use windmill_common::scripts::ScriptRunnableSettingsHandle;
|
||||
use windmill_common::utils::require_admin;
|
||||
use windmill_common::variables::decrypt;
|
||||
use windmill_common::worker::WINDMILL_DIR;
|
||||
use windmill_common::workspaces::WM_FORK_PREFIX;
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{to_anyhow, Error, Result},
|
||||
@@ -119,6 +120,26 @@ pub fn is_none_or_false(val: &Option<bool>) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the keys to strip from trigger/schedule serialization when the
|
||||
/// source workspace is a fork. Stripping these keys avoids propagating
|
||||
/// fork-local operational state (enabled flag, runtime listener identifiers)
|
||||
/// back to the parent workspace through the git-sync round-trip.
|
||||
fn fork_trigger_ignore_keys(w_id: &str) -> Option<Vec<&'static str>> {
|
||||
if w_id.starts_with(WM_FORK_PREFIX) {
|
||||
Some(vec!["mode", "enabled"])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn fork_schedule_ignore_keys(w_id: &str) -> Option<Vec<&'static str>> {
|
||||
if w_id.starts_with(WM_FORK_PREFIX) {
|
||||
Some(vec!["enabled"])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
enum ArchiveImpl {
|
||||
#[cfg(feature = "zip")]
|
||||
Zip(async_zip::tokio::write::ZipFileWriter<tokio::fs::File>),
|
||||
@@ -682,8 +703,11 @@ pub(crate) async fn tarball_workspace(
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let schedule_ignore_keys = fork_schedule_ignore_keys(&w_id);
|
||||
for schedule in schedules {
|
||||
let app_str = &to_string_without_metadata(&schedule, false, None).unwrap();
|
||||
let app_str =
|
||||
&to_string_without_metadata(&schedule, false, schedule_ignore_keys.clone())
|
||||
.unwrap();
|
||||
archive
|
||||
.write_to_archive(&app_str, &format!("{}.schedule.json", schedule.path))
|
||||
.await?;
|
||||
@@ -691,6 +715,27 @@ pub(crate) async fn tarball_workspace(
|
||||
}
|
||||
|
||||
if include_triggers.unwrap_or(false) {
|
||||
#[cfg(any(
|
||||
feature = "http_trigger",
|
||||
feature = "websocket",
|
||||
feature = "postgres_trigger",
|
||||
feature = "mqtt_trigger",
|
||||
feature = "native_trigger",
|
||||
all(
|
||||
feature = "enterprise",
|
||||
any(
|
||||
feature = "kafka",
|
||||
feature = "sqs_trigger",
|
||||
feature = "gcp_trigger",
|
||||
feature = "azure_trigger",
|
||||
feature = "nats",
|
||||
feature = "smtp",
|
||||
),
|
||||
feature = "private"
|
||||
)
|
||||
))]
|
||||
let trigger_ignore_keys = fork_trigger_ignore_keys(&w_id);
|
||||
|
||||
#[cfg(feature = "http_trigger")]
|
||||
{
|
||||
use crate::triggers::http::HttpTrigger;
|
||||
@@ -698,7 +743,9 @@ pub(crate) async fn tarball_workspace(
|
||||
let http_triggers = handler.list_triggers(&mut *tx, &w_id, None).await?;
|
||||
|
||||
for trigger in http_triggers {
|
||||
let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap();
|
||||
let trigger_str =
|
||||
&to_string_without_metadata(&trigger, false, trigger_ignore_keys.clone())
|
||||
.unwrap();
|
||||
archive
|
||||
.write_to_archive(
|
||||
&trigger_str,
|
||||
@@ -715,7 +762,9 @@ pub(crate) async fn tarball_workspace(
|
||||
let websocket_triggers = handler.list_triggers(&mut *tx, &w_id, None).await?;
|
||||
|
||||
for trigger in websocket_triggers {
|
||||
let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap();
|
||||
let trigger_str =
|
||||
&to_string_without_metadata(&trigger, false, trigger_ignore_keys.clone())
|
||||
.unwrap();
|
||||
archive
|
||||
.write_to_archive(
|
||||
&trigger_str,
|
||||
@@ -732,7 +781,9 @@ pub(crate) async fn tarball_workspace(
|
||||
let kafka_triggers = handler.list_triggers(&mut *tx, &w_id, None).await?;
|
||||
|
||||
for trigger in kafka_triggers {
|
||||
let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap();
|
||||
let trigger_str =
|
||||
&to_string_without_metadata(&trigger, false, trigger_ignore_keys.clone())
|
||||
.unwrap();
|
||||
archive
|
||||
.write_to_archive(
|
||||
&trigger_str,
|
||||
@@ -749,7 +800,9 @@ pub(crate) async fn tarball_workspace(
|
||||
let sqs_triggers = handler.list_triggers(&mut *tx, &w_id, None).await?;
|
||||
|
||||
for trigger in sqs_triggers {
|
||||
let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap();
|
||||
let trigger_str =
|
||||
&to_string_without_metadata(&trigger, false, trigger_ignore_keys.clone())
|
||||
.unwrap();
|
||||
archive
|
||||
.write_to_archive(
|
||||
&trigger_str,
|
||||
@@ -766,7 +819,9 @@ pub(crate) async fn tarball_workspace(
|
||||
let gcp_triggers = handler.list_triggers(&mut *tx, &w_id, None).await?;
|
||||
|
||||
for trigger in gcp_triggers {
|
||||
let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap();
|
||||
let trigger_str =
|
||||
&to_string_without_metadata(&trigger, false, trigger_ignore_keys.clone())
|
||||
.unwrap();
|
||||
archive
|
||||
.write_to_archive(
|
||||
&trigger_str,
|
||||
@@ -783,7 +838,9 @@ pub(crate) async fn tarball_workspace(
|
||||
let azure_triggers = handler.list_triggers(&mut *tx, &w_id, None).await?;
|
||||
|
||||
for trigger in azure_triggers {
|
||||
let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap();
|
||||
let trigger_str =
|
||||
&to_string_without_metadata(&trigger, false, trigger_ignore_keys.clone())
|
||||
.unwrap();
|
||||
archive
|
||||
.write_to_archive(
|
||||
&trigger_str,
|
||||
@@ -801,7 +858,8 @@ pub(crate) async fn tarball_workspace(
|
||||
|
||||
for trigger in nats_triggers {
|
||||
let trigger_str: &String =
|
||||
&to_string_without_metadata(&trigger, false, None).unwrap();
|
||||
&to_string_without_metadata(&trigger, false, trigger_ignore_keys.clone())
|
||||
.unwrap();
|
||||
archive
|
||||
.write_to_archive(
|
||||
&trigger_str,
|
||||
@@ -818,7 +876,9 @@ pub(crate) async fn tarball_workspace(
|
||||
let postgres_triggers = handler.list_triggers(&mut *tx, &w_id, None).await?;
|
||||
|
||||
for trigger in postgres_triggers {
|
||||
let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap();
|
||||
let trigger_str =
|
||||
&to_string_without_metadata(&trigger, false, trigger_ignore_keys.clone())
|
||||
.unwrap();
|
||||
archive
|
||||
.write_to_archive(
|
||||
&trigger_str,
|
||||
@@ -835,7 +895,9 @@ pub(crate) async fn tarball_workspace(
|
||||
let mqtt_triggers = handler.list_triggers(&mut *tx, &w_id, None).await?;
|
||||
|
||||
for trigger in mqtt_triggers {
|
||||
let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap();
|
||||
let trigger_str =
|
||||
&to_string_without_metadata(&trigger, false, trigger_ignore_keys.clone())
|
||||
.unwrap();
|
||||
archive
|
||||
.write_to_archive(
|
||||
&trigger_str,
|
||||
@@ -852,7 +914,9 @@ pub(crate) async fn tarball_workspace(
|
||||
let email_triggers = handler.list_triggers(&mut *tx, &w_id, None).await?;
|
||||
|
||||
for trigger in email_triggers {
|
||||
let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap();
|
||||
let trigger_str =
|
||||
&to_string_without_metadata(&trigger, false, trigger_ignore_keys.clone())
|
||||
.unwrap();
|
||||
archive
|
||||
.write_to_archive(
|
||||
&trigger_str,
|
||||
@@ -872,11 +936,16 @@ pub(crate) async fn tarball_workspace(
|
||||
list_native_triggers(&mut *tx, &w_id, service_name, None, None, None, None)
|
||||
.await?;
|
||||
|
||||
let mut native_ignore_keys = vec!["webhook_token_hash"];
|
||||
if let Some(ref extra) = trigger_ignore_keys {
|
||||
native_ignore_keys.extend_from_slice(extra);
|
||||
}
|
||||
|
||||
for trigger in native_triggers {
|
||||
let trigger_str = &to_string_without_metadata(
|
||||
&trigger,
|
||||
false,
|
||||
Some(vec!["webhook_token_hash"]),
|
||||
Some(native_ignore_keys.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
archive
|
||||
|
||||
@@ -557,7 +557,7 @@ async fn update_trigger<T: TriggerCrud>(
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((workspace_id, path)): Path<(String, StripPath)>,
|
||||
Json(edit_trigger): Json<TriggerData<T::TriggerConfigRequest>>,
|
||||
Json(mut edit_trigger): Json<TriggerData<T::TriggerConfigRequest>>,
|
||||
) -> Result<String> {
|
||||
let path = path.to_path();
|
||||
check_scopes(&authed, || {
|
||||
@@ -574,6 +574,24 @@ async fn update_trigger<T: TriggerCrud>(
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
// When the request omits `mode`/`enabled`, preserve the existing DB value
|
||||
// instead of falling back to the BaseTriggerData default (Enabled). This
|
||||
// keeps fork→parent git-sync round-trips from flipping the parent's
|
||||
// operational state — see fork_trigger_ignore_keys in workspaces_export.rs.
|
||||
if edit_trigger.base.is_mode_unspecified() {
|
||||
let existing_mode: Option<TriggerMode> = sqlx::query_scalar(&format!(
|
||||
"SELECT mode FROM {} WHERE workspace_id = $1 AND path = $2",
|
||||
T::TABLE_NAME
|
||||
))
|
||||
.bind(&workspace_id)
|
||||
.bind(path)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
if let Some(m) = existing_mode {
|
||||
edit_trigger.base.set_mode(m);
|
||||
}
|
||||
}
|
||||
|
||||
let new_path = edit_trigger.base.path.to_string();
|
||||
let labels = edit_trigger.base.labels.clone();
|
||||
let on_behalf_of_info = windmill_common::check_on_behalf_of_preservation(
|
||||
|
||||
@@ -128,6 +128,21 @@ impl BaseTriggerData {
|
||||
)
|
||||
}
|
||||
|
||||
/// True when neither `mode` nor the legacy `enabled` field was provided in
|
||||
/// the request. Used by the update path to distinguish "explicitly Enabled"
|
||||
/// from "missing — preserve existing value", which matters for git-sync
|
||||
/// round-trips through fork workspaces (see workspaces_export.rs).
|
||||
pub fn is_mode_unspecified(&self) -> bool {
|
||||
#[allow(deprecated)]
|
||||
{
|
||||
self.mode.is_none() && self.enabled.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_mode(&mut self, mode: TriggerMode) {
|
||||
self.mode = Some(mode);
|
||||
}
|
||||
|
||||
pub fn resolve_permissioned_as(&self, authed: &impl Authable) -> String {
|
||||
if let Some(ref permissioned_as) = self.permissioned_as {
|
||||
if self.preserve_permissioned_as.unwrap_or(false)
|
||||
|
||||
Reference in New Issue
Block a user