fix(git-sync): runtime license gate for auto-pull saves; user/group promotion-branch parity

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PP5gBSPfo1YtkL1sWVAjJm
This commit is contained in:
hugocasa
2026-07-08 14:36:06 +02:00
parent 41cc2072ac
commit 132197e30c
2 changed files with 59 additions and 0 deletions
@@ -2753,6 +2753,23 @@ fn cleanup_legacy_git_sync_settings_in_memory(
#[cfg(not(feature = "enterprise"))]
const CE_GIT_SYNC_MAX_USERS: i64 = 2;
/// Auto-pull is licensed per plan, not just per build: the poller only serves
/// Enterprise plans at runtime, so the save path must reject the setting too —
/// otherwise an EE binary without the plan could still register a webhook and
/// receive webhook-driven pulls.
#[cfg(feature = "enterprise")]
async fn check_auto_pull_license() -> Result<()> {
if !matches!(
windmill_common::ee_oss::get_license_plan().await,
windmill_common::ee_oss::LicensePlan::Enterprise
) {
return Err(Error::BadRequest(
"Automatic pull from git requires an Enterprise license".to_string(),
));
}
Ok(())
}
#[cfg(feature = "enterprise")]
async fn check_git_sync_access(_db: &DB, _w_id: &str) -> Result<()> {
Ok(())
@@ -2886,6 +2903,14 @@ async fn edit_git_sync_config(
"Automatic pull from git is an Enterprise Edition feature".to_string(),
));
}
#[cfg(feature = "enterprise")]
if git_sync_settings
.repositories
.iter()
.any(|r| r.auto_pull.as_ref().is_some_and(|a| a.enabled))
{
check_auto_pull_license().await?;
}
// Preserve server-owned auto-pull state (webhook id/secret, synced sha, last
// status) that the redacted GET response omits — otherwise a whole-config
// save from the UI would drop the webhook secret (breaking delivery) or
@@ -3083,6 +3108,15 @@ async fn edit_git_sync_repository(
"Automatic pull from git is an Enterprise Edition feature".to_string(),
));
}
#[cfg(feature = "enterprise")]
if new_config
.repository
.auto_pull
.as_ref()
.is_some_and(|a| a.enabled)
{
check_auto_pull_license().await?;
}
// Promotion mode: EE only
#[cfg(not(feature = "enterprise"))]
@@ -920,6 +920,11 @@ fn git_sync_deploy_pr_head_branch(
if !use_individual_branch {
return None;
}
// Mirrors the CLI's computeGitSyncDeployBranch: user/group objects are
// pushed to the base branch and never get their own wm_deploy branch.
if path_type == "user" || path_type == "group" {
return None;
}
let git_ref = if !item_path.is_empty() {
item_path
} else {
@@ -1866,6 +1871,26 @@ pub fn extract_error_value(
mod git_sync_pr_tests {
use super::{git_sync_deploy_pr_head_branch, git_sync_push_result_pushed};
#[test]
fn user_and_group_items_get_no_promotion_branch() {
for path_type in ["user", "group"] {
assert_eq!(
git_sync_deploy_pr_head_branch(
"ws",
None,
None,
"main",
true,
false,
"u/someone",
"",
path_type
),
None
);
}
}
#[test]
fn push_result_pushed_flag() {
assert_eq!(