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:
hugocasa
2026-04-29 18:35:19 +02:00
co-authored by Claude Opus 4.7
parent 4098793db2
commit a4d8f138ae
3 changed files with 115 additions and 13 deletions
+19 -1
View File
@@ -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(
+15
View File
@@ -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)